0

I am attempting to test a class containing a member of third party library.

I have created a fake library to mimic the aforementioned, and am using preprocessor directives to chose between the "real" and fake libraries at compile time - real for the production compilation unit and fake for the test.

The problem is that I see some discrepancies between what comes out of the preprocessor and what the linker produces.

Here's an extremely cut-down version of the setup I have:

Third-party library - lib.hpp

This is a header-only library, so all functions are inlined.

namespace lib {

class Foo {
public:
  int returnSomething() const { return 42; }
};

}

Fake library

In the actual case it's fairly chunky so I've split it in header and source files:

Header - fake_lib.hpp

namespace fake {

class FooBar {
public:
  int returnSomething() const;
};

}

Source - fake_lib.cpp

#include "fake_lib.hpp"

namespace fake {

int FooBar::returnSomething() const {
  return 13;
}

}

Class under test - bar.hpp and bar.cpp

#ifdef SWITCH
#include "fake_lib.hpp"
#endif

// There is no #else here for the preprocessor because in my actual use case we
// need some times from the third_party libs whether we test or are in production
#include "lib.hpp"

class Bar {
public:
  int get();

private:
#ifndef SWITCH
#warning "real"
  lib::Foo m_foo;
#else
#warning "fake"
  fake::FooBar m_foo;
#endif

};
#include "bar.hpp"

int Bar::get() {
  return m_foo.returnSomething();
}

Production exe

#include "bar.hpp"

#include <iostream>

int main() {
  Bar bar;

  std::cout << bar.get() << std::endl;

  return 0;
}

Test exe

#include "bar.hpp"

#include <iostream>

int main() {
  Bar bar;

  std::cout << bar.get() << std::endl;

  return 0;
}

This is all built via CMake, here's my CMakeLists.txt:

cmake_minimum_required(VERSION 3.12)

project(fake_failure LANGUAGES CXX)

add_library(lib INTERFACE)
target_include_directories(lib INTERFACE ${CMAKE_CURRENT_SOURCE_DIR}/lib)

add_library(fake OBJECT fake/fake_lib.cpp)
target_include_directories(fake PUBLIC ${CMAKE_CURRENT_SOURCE_DIR}/fake)

add_library(bar OBJECT bar.cpp)
target_include_directories(bar PUBLIC ${CMAKE_CURRENT_SOURCE_DIR})
target_link_libraries(bar PUBLIC lib) 

add_executable(main_exe main.cpp)
target_link_libraries(main_exe PRIVATE bar)

add_executable(fake_exe fake.cpp)
target_compile_definitions(fake_exe PRIVATE SWITCH)
target_link_libraries(fake_exe PRIVATE bar fake)

Desired outcome

When main_exe is run, it should print 42. When fake_exe is run, it should print 13.

Actual outcome

fake_exe prints out 42 as well.

Now, just to get this out of the way - I can fix this and get the desired outcome for now by defining a static fake_bar library which adds the SWITCH macro.

What I'd like to find out

When I run the preprocessor manually I see that Bar's m_foo member if of the fake type. I assume since the bar library is compiled without the SWITCH macro it uses the inline definitions, meaning that the produced object file has the inlined functions as symbols. Why doesn't the linker error out if it can't see symbols for the fake type?

And why is it that when I run gdb, break in Bar and do print typeid(m_foo) I see it is of the real type, even though the preprocessor output I mentioned above said otherwise.

The above would explain why the linker doesn't produce errors - it doesn't even attempt to use the fake symbols. But I can't understand why.

Thanks in advance!

Pesho_T
  • 814
  • 1
  • 6
  • 18

1 Answers1

0

You are breaking the ODR rule. The definition of Bar class type in object file generated from fake.cpp and bar.cpp consists of different sequence of tokens - specifically, the member Bar::m_foo has different type in both translation units.

The behavior of your program fake_exe is undefined.

I assume since the bar library is compiled without the SWITCH macro it uses the inline definitions, meaning that the produced object file has the inlined functions as symbols.

No, it does not mean that. The keyword inline only encourages compiler to inline the function. The compiler is allowed not to inline the function. cppreference inline

Anyway, Bar::get() is most probably not getting inlined by your compiler because it's in different translation unit (but could be inlined anyway). Since Bar::get() "sees" lib::Foo m_foo; that means the compiler will generate a call for lib::Foo::returnSomething() in Bar::get(), thus Bar::get() will return 42. Inspect the generated assembly code to really know what the compiler is doing with your code.

Why doesn't the linker error out if it can't see symbols for the fake type?
why the linker doesn't produce errors

Because linker has no information to diagnose and produce such errors. Linker has no information about the type of symbols. Linker sees almost only symbols names.

KamilCuk
  • 120,984
  • 8
  • 59
  • 111
  • `Foo::returnSomething()` would have a different symbol name from `FooBar::returnSomething()`, so no ODR breach happens, main.o simply won't have a reference to the latter. CMake doesn't rebuild object files based on definitions change if target is shared between project. Have to be separate targets, or build system wouldn't know if it need to rebuild or not. – Swift - Friday Pie Mar 20 '20 at 12:49
  • I'm sorry, I do not get your. I do not care about different symbol name. I care about sequence of tokens when defining `Bar` class type. `fake.cpp` has `SWITCH` macro defined while `bar.cpp` doesn't. `fake.cpp` sees `fake::FooBar m_foo;` while `bar.cpp` sees `lib::Foo m_foo;`. `CMake doesn't rebuild object files based on definitions change` - exactly, and because there is no `target_compile_definitions(fake_exe PRIVATE SWITCH)` for `bar` library, that means that `bar.cpp` is build without `SWITCH` macro. – KamilCuk Mar 20 '20 at 12:51
  • `m_foo` in both cases is an internal symbol for main.cpp (`bar` is a local variable). External symbol in second case would be `FooBar::returnSomething()` that should be called with pointer to `m_foo` object. Without switch there is already `Foo::returnSomething()` which got internal linkage or is inlined. – Swift - Friday Pie Mar 20 '20 at 12:54
  • The linkage of the symbol doesn't matter. The `second case` would be compilation of `bar.cpp`, right? – KamilCuk Mar 20 '20 at 12:54
  • it does. if both had eternal linkage an, and both clases had same name, then compiler would generate two symbols with same name. THAT is ODR breach. Interal linkage names can be repeated, no offence happens. method belong to different classes, m_foo not a static member, object is automatic, so it's internal symbol. – Swift - Friday Pie Mar 20 '20 at 12:55
  • Sorry, I do not follow. A class type definition doesn't have linkage - it's a type. A class type definition has different sequence of tokens in two translation units that are compiled together - that's undefined behavior. I don't think I care about what compiler does with that. About which symbol you are reffering to? `m_foo` is a member of class `Bar`, it doesn't "have" linkage (it doesn't "exists"), it's a member of a type, inside a type definition. When you declare `Bar bar;` then the symbol `bar` has internal linkage. Or I am missing something very obvious? – KamilCuk Mar 20 '20 at 13:00
  • It stated that they should be equivalent, not same. It happens so that in this case definitions of class Bar are equivalent, in other case you'd be right. It'd be ill-formed program if ODR didn't exclude internally linked and inline functions (or member functions) from requirement, so it creates a funky pseudo-UB. – Swift - Friday Pie Mar 20 '20 at 13:08
  • Let us [continue this discussion in chat](https://chat.stackoverflow.com/rooms/210001/discussion-between-swift-friday-pie-and-kamilcuk). – Swift - Friday Pie Mar 20 '20 at 13:24