Why C++ linker is silent about ODR violation?
Asked Answered
J

2

7

Let's consider some synthetic but expressive example. Suppose we have Header.h:

Header1.h

#include <iostream>

// Define generic version
template<typename T>
inline void Foo()
{
    std::cout << "Generic\n";
}

Header2.h

void Function1();

Header3.h

void Function2();

Source1.cpp

#include "Header1.h"
#include "Header3.h"

// Define specialization 1
template<>
inline void Foo<int>()
{
    std::cout << "Specialization 1\n";
}

void Function1()
{
    Foo<int>();
}

Later I or some else defines similar conversion in another source file. Source2.cpp

#include "Header1.h"

// Define specialization 2
template<>
inline void Foo<int>()
{
    std::cout << "Specialization 2\n";
}

void Function2()
{
    Foo<int>();
}

main.cpp

#include "Header2.h"
#include "Header3.h"

int main()
{
    Function1();
    Function2();
}

The question is what will print Function1() and Function2()? The answer is undefined behavior.

I expect to see in output: Specialization 1 Specialization 2

But I see: Specialization 2 Specialization 2

Why C++ compilers are silent about ODR violation? I would prefer compilation to be failed in this case.

I found only one workaround: define template functions in unnamed namespace.

Jacqui answered 15/7, 2017 at 16:20 Comment(7)
Compilers (actually linker in this case) are often silent when it comes to undefined behavior. As another workaround you can start using single translation unit.Eli
Compilers/linkers can't diagnose all bugs nor does the language require them to. It is your responsibility as a programmer to follow the rules of the language - the tools just help out sometimes.Strikebound
ODR violation might be hard to track, even for compiler/linker..And it would require lot of long extra works.Disaffiliate
The standard permits the compiler to be silent with undefined behaviour, including ODR. One reason (of several) is that ODR violations can be caused by a definition appearing in two compilation units, and the compiler has no visibility of one compilation unit when compiling the other. In other words, there is no guarantee that a compiler CAN detect ODR, let alone diagnose it. Practically (in a compile-then-link toolchain) a linker can detect some ODR violationss.Harmless
I actually saw something like this happening in more complex form (they were placing specializations into separate headers), so it isn't absolutely synthetic. Linker may not detect it of optimization decided to inline those specializations and there is no conflicting symbols in result.Shrieve
Thank you @Peter, but could you please exaplain what you mean by "one reason (of several)"? What are the other reasons?Phytophagous
@Phytophagous I was referring to the fact that there are numerous ways to break ODR. en.cppreference.com/w/cpp/language/definition lists several conditions that must all be met to comply with ODR, and breaking one or more of them in a program violates that.Harmless
A
10

The compiler is silent, because it's not required to emit anything by [basic.def.odr/4]:

Every program shall contain exactly one definition of every non-inline function or variable that is odr-used in that program outside of a discarded statement; no diagnostic required. The definition can appear explicitly in the program, it can be found in the standard or a user-defined library, or (when appropriate) it is implicitly defined (see [class.ctor], [class.dtor] and [class.copy]). An inline function or variable shall be defined in every translation unit in which it is odr-used outside of a discarded statement.

Armington answered 15/7, 2017 at 16:25 Comment(4)
The link to the C++ reference is not working anymore; here.Justiciary
@caucus, it works fine. But being a link to "trunk", the wording shifted. It now points to the standard version as of the answer being reference.Armington
I'd upvote an answer that explains why it doesn't - rather than one that explains why it's allowed not to. Toolchains emit any amount of helpful information that isn't required. It would be helpful to know why such an valuable diagnostic is absent.Belittle
@JohnMcFarlane - That sentiment would be more impressive if your comment didn't start with mentioning upvotes. I'd consider a comment that just pointed out an answer can be enhanced - rather the one that dangles imaginary internet points in front of me as some cheap carrot to make me do something. Or even better, given your resume you can add an answer yourself, since nothing prevents that.Armington
G
-3

In rare cases it can be useful to violate ODR.

For example, you can instead of std::unique_ptr<MyPimplType> use std::aligned_storage<MyPimplType_sizeof, MyPimplType_alignof> and test the real sizeof and alignment in the constructor/destructor of MyPimplType class. This called aligned storage pimpl or something similar. Useful when you want to replace impl-by-pointer (by smart pointer to impl) by impl-by-value (aligned storage instead of impl).

Next, you can create new type of aligned storage, which can test the sizeof and the alignment automatically in it's constructor/destructor:

private/my_aligned_storage_by_decl.hpp or public/my_aligned_storage_by_decl.hpp

template <typename incomplete_type_to_align, size_t sizeof_, size_t alignof_>
class my_aligned_storage_by { ... };

private/my_aligned_storage_by_impl.hpp

// incomplete_type_to_align must be already complete here!
template <typename incomplete_type_to_align, size_t sizeof_, size_t alignof_>
my_aligned_storage_by<...>::my_aligned_storage_by(...) {
    static_assert(sizeof_ == sizeof(incomplete_type_to_align), ...);
    static_assert(alignof_ == std::alignment_of<incomplete_type_to_align>::value, ...);
}

This can be reached only through the ODR violation AND if public and private headers can not be merged to a single header, where the public and private headers having 2 different definitions of the same my_aligned_storage_by class.

The implementation:
https://github.com/andry81/tacklelib/tree/HEAD/include/tacklelib/tackle/aligned_storage/

Example of usage:

include/myheader.hpp

#include <tacklelib/tackle/aligned_storage/public/aligned_storage_by_decl.hpp>

#define MYCLASS_SIZEOF  ...
#define MYCLASS_ALIGNOF ...

class MyClass
{
  // public methods...
  MyClass(...);

  // replacement as impl-by-value:
  struct This;
  tackle::aligned_storage_by<This,
    MYCLASS_SIZEOF,
    MYCLASS_ALIGNOF,
    tackle::tag_pttn_control_lifetime> m_this;
};

void myfoo(const MyClass & myboo);

src/_impl/myheader_this.hpp

#include <myheader.hpp>

struct MyClass::This
{
  // data members and private function of class MyClass is placed here...
};

src/MyClass.cpp

#include <tacklelib/tackle/aligned_storage/private/aligned_storage_by_decl.hpp>

#include <src/_impl/MyClass_this.hpp>

#include <tacklelib/tackle/aligned_storage/private/aligned_storage_by_impl.hpp>

// public methods implementation...

MyClass::MyClass()
{
  m_this.construct_default();
}

MyClass::MyClass(...)
{
  m_this.construct(This{...});
}

void myfoo(const MyClass & myboo)
{
  auto & realboo = *myboo.m_this.this_();
  // deal with realboo...
}

Here is some major drawbacks of such approach:

  1. You have to split headers containing classes with my_aligned_storage as members into public/private headers too and leave public headers for SDK, but include private one into cpp files instead of public. (*)

  2. You have explicitly control the order of include of such headers, because private header can be silently included instead of the public (but not vice versa).

  3. You have to include implementation with sizeof/alignment test asserts only when the type become fully complete, which is sometimes is not always possible.

  4. You have explicitly specify sizeof and alignment, which can be different in different contexts, for example, debug/release, windows/linux, msvc/gcc and so on.

(*) In case if public and private header of the my_aligned_storage can not be merged into single public declaration header.

These drawbacks can be avoided or ignored in cases, where an aligned user class is really not big and frequently constructed/assigned/copied, like a builtin type.

Governance answered 15/8, 2018 at 14:47 Comment(0)

© 2022 - 2024 — McMap. All rights reserved.