Search code examples
c++cmakeincludemingwundefined-reference

C++ / CMake / Conan.io - 'Undefined Reference to Class::Method' when including header


I'm having a little trouble when compiling a project, using Conan.io and CMake.

I'm building a small OpenGL-based project. I use an MVC architecture. I want CMake to produce two distinct .exe :

  • main.exe being a small GLFW window with a simple OpenGL context. It builds and works totally well, using conan.io to manage the libs used.
  • pentest.exe being a simple test executable which I want to use to test some basics functions from my model. This one won't be compiled when I call the make command.

Here my simplified project architecture :

├───build
│   ├───.cmake
│   │
│   ├───bin
│   │   └─── // .exe files are here
│   │
│   ├───CMakeFiles
│   │   └─── // Cmake files are here
│   │
│   └─── // ...
│
├───include
│   ├───GL
│   │   └─── GLU.h
│   │
│   └───model
│       ├───Block.hpp
│       └───GameGrid.hpp
│   
├───src
│   ├───model
│   │   ├───Block.hpp
│   │   └───GameGrid.hpp
│   │
│   ├───main.cpp
│   └───pentest.cpp
│
├───CMakeLists.txt
└───conanfile.txt

Please note that :

  • pentest.cpp doesn't rely on any external libs.
  • Even though my GameGrid class is a template class, I made the header include the implementation file at the end (following this StackOverflow question).
  • I'm very, very bad with CMake.
  • CMake command is doing very well, the errors are occuring when the make command is calling the linker for pentest.exe.

Here is my CMakeLists.txt :

cmake_minimum_required(VERSION 2.8.12)
project(TheEndless)
    
add_definitions("-std=c++17")
    
include(${CMAKE_BINARY_DIR}/conanbuildinfo.cmake)
conan_basic_setup()
    
include_directories(
        ${PROJECT_SOURCE_DIR}/include
        ${PROJECT_SOURCE_DIR}/src
        ${PROJECT_SOURCE_DIR}/src/model
)
    
link_directories(${CMAKE_SOURCE_DIR}/lib)
    
add_executable(main src/main.cpp)
add_executable(pentest src/pentest.cpp)
target_link_libraries(main ${CONAN_LIBS})
target_link_libraries(pentest ${CONAN_LIBS})

Here is my pentest.cpp :

#include <iostream>
#include <string>

#include "model/Block.hpp"
#include "model/GameGrid.hpp"


int main(int argc, char const *argv[]) {
    theendless::model::Block b;
    theendless::model::GameGrid<1, 1> g;

    g(0, 0) = b;
    std::string s(g(0, 0).get_name());

    std::cout << s << std::endl;

    return 0;
}

Here is my model/Block.hpp :

#ifndef THEENDLESS_MODEL_BLOCK_HPP
#define THEENDLESS_MODEL_BLOCK_HPP


#include <string>


namespace theendless::model {
    class Block {
        private:
            std::string name;

        public:
            Block();
            Block(std::string name);

            std::string get_name() const;

            void set_name(const std::string newName);
    };
}


#endif

Here is my model/Block.cpp:

#include "model/Block.hpp"

#include <string>

namespace theendless::model {
    Block::Block() : name("default_name") {}
    Block::Block(std::string name) : name(name) {}
    
    std::string Block::get_name() const { return this->name; }

    void Block::set_name(const std::string newName) { this->name = newName; }
}

Here is the errors that are shown by make :

PS C:\projects\TheEndless\build> make
Scanning dependencies of target pentest
[ 75%] Building CXX object CMakeFiles/pentest.dir/src/pentest.cpp.obj
[100%] Linking CXX executable bin/pentest.exe
c:/mingw/bin/../lib/gcc/x86_64-w64-mingw32/9.2.0/../../../../x86_64-w64-mingw32/bin/ld.exe: CMakeFiles/pentest.dir/objects.a(pentest.cpp.obj): in function `main':
C:/projects/TheEndless/src/pentest.cpp:9: undefined reference to `theendless::model::Block::Block()'
c:/mingw/bin/../lib/gcc/x86_64-w64-mingw32/9.2.0/../../../../x86_64-w64-mingw32/bin/ld.exe: C:/projects/TheEndless/src/pentest.cpp:13: undefined reference to `theendless::model::Block::get_name[abi:cxx1c:/mingw/bin/../lib/gcc/x86_64-w64-mingw32/9.2.0/../../../../x86_64-w64-mingw32/bin/ld.exe: CMakeFiles/pentest.dir/objects.a(pentest.cpp.obj): in function `std::array<theendless::model::Block, 1ull>::array()':
c:/mingw/include/c++/9.2.0/array:94: undefined reference to `theendless::model::Block::Block()'
collect2.exe: error: ld returned 1 exit status
make[2]: *** [CMakeFiles/pentest.dir/build.make:107: bin/pentest.exe] Error 1
make[1]: *** [CMakeFiles/Makefile2:124: CMakeFiles/pentest.dir/all] Error 2
make: *** [Makefile:103: all] Error 2

Note that including model/Block.cpp to pentest.cpp makes the problem disappear, but I kinda want to make the project be clean, so I would like not to do this.

Additional infos :

  • I'm on windows 10.
  • I'm using VSCode to edit my files, and I compile by typing make in the integrated *PowerShell terminal.
  • I'm using a Mingw distro to compile.

Any help would be greatly appreciated ! :)


Solution

  • I'm not a CMake or compiler expert, but here's how I understand what's going on.

    The compiler does not search around for any headers or source files unless it is told that it has to. But when does the compiler have to search for them?

    1. The file (usually headers) are included in a file that the compiler already knows about.
    2. The file has explicitly been made known to the compiler (e.g. inside a CMakeLists.txt).

    In your case, the compiler knows about the header file, because it was #included inside the pentest.cpp source file (variant 1. from above). And how did the compiler know about pentest.cpp? It was explicitly stated inside the function add_executable that the specific build target pentest is built from this file.

    Now what about Block.cpp? It was not known to the compiler because it neither was included nor stated inside the CMakeLists.txt, that the compiler has to use this file. So the compiler cannot know about it.

    As you already mentioned, including the .cpp file is not a good style. One huge advantage (in my opinion) of only including header files is that if the implementation of a function changes (not its declaration, but the body of the function) you don't have to recompile everything where it's used. You just have to recompile that one .cpp file.

    So what's the solution? You have to make the compiler be aware of all the source files that should be used to build your target pentest. Therefore, you have to add those source files to the function add_executable. The CMake docs for add_executable tell you the following.

    add_executable(<name> [WIN32] [MACOSX_BUNDLE]
                   [EXCLUDE_FROM_ALL]
                   [source1] [source2 ...])
    

    Adds an executable target called to be built from the source files listed in the command invocation.

    So you have to add all the source files which shall be used for building your target to the same add_executable command invocation. In your case, the CMakeLists.txt would look like this. Note that I had to add target_compile_features and remove add_compile_definitions on my machine in order to enforce the usage of C++17.

    cmake_minimum_required(VERSION 2.8.12)
    project(TheEndless)
        
    # add_definitions("-std=c++17") # does not work on my Windows 10 machine
        
    include(${CMAKE_BINARY_DIR}/conanbuildinfo.cmake)
    conan_basic_setup()
        
    include_directories(
            ${PROJECT_SOURCE_DIR}/include
            ${PROJECT_SOURCE_DIR}/src
            ${PROJECT_SOURCE_DIR}/src/model
    )
        
    link_directories(${CMAKE_SOURCE_DIR}/lib)
        
    add_executable(main src/main.cpp)
    # add all source files needed to build to the executable
    # GameGrid.cpp is not needed because it is included in the header.
    add_executable(pentest src/pentest.cpp src/model/Block.cpp)
    target_link_libraries(main ${CONAN_LIBS})
    target_link_libraries(pentest ${CONAN_LIBS})
    
    target_compile_features(pentest PRIVATE cxx_std_17) # added to really use C++17, see the docs for explanation
    

    Only "problem" I saw: Visual Studio marked Block.hpp, GameGrid.hpp, and GameGrid.cpp as "external dependencies". If you want them to be shown as part of your target, you also may add them to add_executable. I'm not sure if there is another solution as it seems to be a little bit redundant.