c++one-definition-rule

Why can duplicate definitions of a class in different cpp files be linked with no error?


In src1.cpp, I define a class names "Test" and a function names "f1" that creates a instance of Test.

// src1.cpp
#include <iostream>

class Test {
    int a = 1;

public:
    Test() {
        std::cout << "src1 Test\n";
    }
};

void f1() {
    Test t;
}

In src1.h, "f1" is exposed.

// src1.h

#pragma once

void f1();

In much the same way, src2.cpp and src2.h is created. A class with the same name and a function that constructs an instance of it.

// src2.cpp

#include <iostream>

class Test {
    long a;

public:
    Test() {
        std::cout << "src2 Test\n";
    }
};

void f2() {
    Test t;
}
// src2.h

#pragma once

void f2();

Then in the main.cc, I call both f1 and f2.

// main.cpp

#include "src1.h"
#include "src2.h"

int main() {
    f1();
    f2();
    return 0;
}

I compile by the following command with no warning and error. g++ -Wall -o main main.cpp src1.cpp src2.cpp And the program output is:

src1 Test  
src1 Test  

It seems compiler allow the different definition of the class Test and both f1 and f2 call the constructor of Test in src1.cpp.

And when I compile with the reverse order like g++ -Wall -o main main.cpp src1.cpp src2.cpp And the program output changes to:

src2 Test  
src2 Test  

When I replace the class Test with a duplicate variable, the compile error occurs. How does linker deal with the duplicate definitions in that case?


Solution

  • Your code is not allowed, even though you are getting no warning and error.

    // a.cpp
    class Test {
        int a = 1;
        // ...
    };
    
    // b.cpp
    class Test {
        long a;
        // ...
    };
    

    This code violates the one-definition-rule (ODR), because the definitions of Test must always be the same in different translation units (TUs) (i.e. .cpp files).

    However, odr-violations are a case of ill-formed, no diagnostic required (IFNDR). This means that your code is not valid C++, but the compiler is not required to issue any warning or error. C++ actually has a large amount of these IFNDR situations.

    The wording in the C++ standard is this:

    For any definable item D with definitions in multiple translation units,

    • if D is a non-inline non-templated function or variable, or
    • if the definitions in different translation units do not satisfy the following requirements,

    the program is ill-formed; [...]

    • Each such definition shall consist of the same sequence of tokens

    - [basic.def.odr]/14

    This means that all definitions of T must be completely identical, essentially copied/pasted versions of each other.

    Solution

    It is very difficult to ensure that you don't violate the ODR manually. This is why you typically put types into headers, so they are guaranteed to be the same in all the cpp files which include them:

    // test.hpp
    class Test {
        int a;
    };
    
    // a.cpp
    #include "test.hpp"
    
    // b.cpp
    #include "test.hpp"
    

    Alternatively, if you want to reuse the Test name but give it different definitions:

    // a.cpp
    namespace { // anonymous namespace
    class Test {
        int a = 1;
        // ...
    };
    }
    
    // b.cpp
    namespace {
    class Test {
        long a;
        // ...
    };
    }
    

    This also solves the problem, because the two Test classes have internal linkage, meaning that the Test in a.cpp and b.cpp refer to different types. Since they are different types, they can also be defined differently.

    Why can't the linker detect this, but detect duplicate variables?

    The reason is simple: types aren't entities that get emitted like functions and variables, they just exist in the program. They still have to be defined the same everywhere, but the definition of Test alone produces no assembly at all.

    Even if it id, it is difficult to detect an odr-violation, because Test can be defined in multiple places. It just needs to be defined the same way. The linker would have to somehow test for equality between two classes, but the C++ standard has very low requirements for linkers. You only get guaranteed diagnostics when using C++20 modules.

    See Also