2

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?

Jan Schultke
  • 17,446
  • 6
  • 47
  • 96
  • 1
    Definitions of the class must be semantically *identical* otherwise your code yields *undefined behaviour* (which is the case here). Have you run that programme? Result *might* be just seeing either of `1` or `2` in the output, though anything else could occur as well. – Aconcagua Jun 30 '23 at 13:09
  • 5
    You have [ODR](https://en.cppreference.com/w/cpp/language/definition#One_Definition_Rule) violation, the program is ill-formed no diagnostic required. – Jarod42 Jun 30 '23 at 13:09
  • *'So, how the linker do with the duplicate definition.'* – under the hoods it most likely would simply use the first compiled variant it encounters during linking. That gets critical especially if you have conflicting types in two different libraries. – Aconcagua Jun 30 '23 at 13:14
  • You can use the [ORC](https://github.com/adobe/orc) tool to detect this ODR ill-formed no diagnostic required situation. – Eljay Jun 30 '23 at 13:43
  • Your test case constructors are one-liners, only used once. It is not at all unreasonable for the compiler to inline these. In that case, the linker will only see references to cout, and nothing about the Test classes. – BoP Jun 30 '23 at 14:13
  • Classes don't get linked. Code and data get linked. – Pete Becker Jun 30 '23 at 15:07

2 Answers2

3

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

Jan Schultke
  • 17,446
  • 6
  • 47
  • 96
-1

The behavior of the program is undefined.

The classes' definitions including their inline constructors shall consist from the same tokens but they have different tokens and entities.

For example in the C++14 Standard there is written 3.2 One definition rule)

6 There can be more than one definition of a class type (Clause 9), enumeration type (7.2), inline function with external linkage (7.1.2), class template (Clause 14), non-static function template (14.5.6), static data member of a class template (14.5.1.3), member function of a class template (14.5.1.1), or template specialization for which some template parameters are not specified (14.7, 14.5.5) in a program provided that each definition appears in a different translation unit, and provided the definitions satisfy the following requirements. Given such an entity named D defined in more than one translation unit, then

(6.1) — each definition of D shall consist of the same sequence of tokens; and

and so on and then

If D is a template and is defined in more than one translation unit, then the preceding requirements shall apply both to names from the template’s enclosing scope used in the template definition (14.6.3), and also to dependent names at the point of instantiation (14.6.2). If the definitions of D satisfy all these requirements, then the behavior is as if there were a single definition of D. If the definitions of D do not satisfy these requirements, then the behavior is undefined.

Vlad from Moscow
  • 301,070
  • 26
  • 186
  • 335
  • It's not undefined behavior, it's a case of ill-formed, no diagnostic required. The section of the standard you're quoting states that. – Jan Schultke Jun 30 '23 at 13:25
  • @JanSchultke Do not bother. We, beginners, are always glad to help software engineers to learn C++.:) – Vlad from Moscow Jun 30 '23 at 13:29
  • 1
    @JanSchultke I don't think UB and IFNDR are mutually exclusive. In fact included in the description of IFNDR on https://en.cppreference.com/w/cpp/language/ub : *The behavior is undefined if such program is executed.* So though your identification of IFNDR is more precise, it is also consistent with calling it UB. – Matt Jun 30 '23 at 13:47
  • @Matt I provided a quote from the C++14 Standard (that in fact is the same in all subsequent C++ Standards) where there is clear written that the behavior is undefined. It is a trick at SO to down-vote other answers to make an impression that they are incorrect except your own answer.:) Here is another such a case, see https://stackoverflow.com/questions/76587545/not-able-to-assign-64-bit-value-to-uint64-t-in-c/76588095#76588095 It is funny that the indeed incorrect answer was even two times upvoted when my correct answer was down-voted.:) – Vlad from Moscow Jun 30 '23 at 13:52
  • @Matt yes, fair point. The standard doesn't say what happens when executing ill-formed programs afaik, so presumably it's UB by omission. I still think it's useful to draw a distinction, because the program is wrong even if you don't execute it. UB is really more of a runtime thing. – Jan Schultke Jun 30 '23 at 14:04
  • @JanSchultke You made my day.:) By the way in teh question ijs written that "I compile by the following command with no warning and error". So what will happen at runtime?:) – Vlad from Moscow Jun 30 '23 at 14:08
  • @VladfromMoscow it would be UB. Still, it's kind of like saying *"water is blue"*. It would be more correct to say that water is transparent, but in the event that the sky is reflected in it, it looks blue. There's value in being precise, especially when the context requires it. – Jan Schultke Jun 30 '23 at 14:13
  • Vlad: @Jan in this comments thread implies that you might like to know why your posts are not better received. I would be pleased if this was indeed so. My feedback to you, in a thread where others can see it, is that your posts are too sloppy. I think you're treating Stack Overflow as a chatroom, and/or composing your posts in a hurry, or perhaps they are written your phone. For a 300K user you're creating a lot of edit work - large numbers of misspellings in some posts, stylistic typographical errors added wilfully, etc. – halfer Jul 02 '23 at 18:29
  • There is a sense from Meta Stack Overflow - including from moderators - that technical writing is an expectation here. The site is more like Wikipedia than Facebook - posts are for posterity, and material ought to be made as readable as possible (curation strikes notwithstanding, of course). – halfer Jul 02 '23 at 18:30