Search code examples
c++namespacesg++

Why does defining functions in header files create multiple definition errors, but not classes?


This question builds off these two stackoverflow posts:

Here's the question: Why doesn't the multiple definition error appear for classes/structs/enums? Why does it only apply to functions or variables?

I wrote some example code in an effort to capture my confusion. There are 4 files: namespace.h, test.h, test.cpp, and main.cpp. The first file is included in both test.cpp and main.cpp, which leads to the multiple definition error if the correct lines are uncommented.

// namespace.h
#ifndef NAMESPACE_H
#define NAMESPACE_H

namespace NamespaceTest {
    // 1. Function in namespace: must be declaration, not defintion
    int test(); // GOOD

    // int test() { // BAD
    //    return 5;
    //}

    // 2. Classes can live in header file with full implementation
    // But if the function is defined outside of the struct, it causes compiler error
    struct TestStruct {
        int x;
        int test() { return 10; } // GOOD
    };

    //int TestStruct::test() { // BAD
    //    return 10;
    //}

    // 3. Variables are also not spared from the multiple definition error.
    //int x = 20; // BAD

    // 4. But enums are perfectly safe.
    enum TestEnum { ONE, TWO }; // GOOD
}

#endif
// test.h
#ifndef TEST_H
#define TEST_H

class Test {
public:
    int test();
};
#endif
// test.cpp
#include "test.h"
#include "namespace.h"

int NamespaceTest::test() {
    return 5;
}

int Test::test() {
    return NamespaceTest::test() + 1;
}
// main.cpp
#include <iostream>
#include "namespace.h"
#include "test.h"

int main() {
    std::cout << "NamespaceTest::test: " << NamespaceTest::test() << std::endl;

    Test test;
    std::cout << "Test::test: " <<test.test() << std::endl;

    NamespaceTest::TestStruct test2;
    std::cout << "NamespaceTest::TestStruct::test: " << test2.test() << std::endl;

    std::cout << "NamespaceTest::x: " << NamespaceTest::TestEnum::ONE << std::endl;
}

g++ test.cpp main.cpp -o main.out && ./main.out

NamespaceTest::test: 5
Test::test: 6
NamespaceTest::TestStruct::test: 10
NamespaceTest::x: 0

Solution

  • Generally when you compile a definition that is namespace scoped (like functions or global variables), your compiler will emit a global symbol for it. If this appears in multiple translation units, there will be a conflict during link-time since there are multiple definitions (which happen to be equivalent, but the linker can't check this).

    This is part of the one definition rule: Exactly one definition of a function or variable is allowed in the entire program, in one of the translation units.

    There are some exceptions to this, for example, class definitions and inline functions/variables. However, definitions must be the exact same (textually) in all the translation units they appear in. Class definitions are meant to be #included, so it makes sense to allow them to appear in multiple translation units.

    If you define a member function inside the class body they are implicitly inline because otherwise you would not be able to include the class definition with the member function definition without breaking ODR. For example, these three are functionally equivalent:

    struct TestStruct {
        int x;
        int test() { return 10; }
    };
    
    // Could have been written
    
    struct TestStruct {
        int x;
        inline int test() { return 10; }
    };
    
    // Or as
    
    struct TestStruct {
        int x;
        int test();  // The `inline` specifier could also be here
    };
    
    inline int TestStruct::test() { return 10; }
    

    You can do this to your namespace scoped functions/variables too: inline int test() { return 5; } and inline int x = 20; would have compiled with no further issue.

    This is implemented by the compiler emitting "specially marked" symbols for inline entities, and the linker picking one arbitrarily since they should all be the same.

    The same exception to ODR also exists for templated functions / variables and enum declarations, since they are also meant to live in header files.