为什么C++链接器对ODR冲突保持沉默?

2exbekwf  于 2023-03-25  发布在  其他
关注(0)|答案(2)|浏览(105)

让我们考虑一些合成但富有表现力的例子。假设我们有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>();
}

后来我或其他人在另一个源文件中定义了类似的转换。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();
}

问题是什么将打印Function1()和Function2()?答案是未定义的行为。
我希望在输出中看到:专业化1专业化2
但我明白了:专业化2专业化2
为什么C++编译器对ODR冲突保持沉默?我宁愿在这种情况下编译失败。
我只找到一个解决方法:在未命名的命名空间中定义模板函数。

0lvr5msh

0lvr5msh1#

编译器是静默的,因为它不是[basic.def.odr/4]发出任何东西所必需的:
每一个程序都必须包含一个非内联函数或变量的定义,这些函数或变量在程序中被丢弃的语句之外被odr使用;**不需要诊断。**定义可以显式出现在程序中,可以在标准或用户定义库中找到,或者(在适当的情况下)隐式定义(参见[class.ctor],[class.dtor]和[class.copy])。内联函数或变量应在每个转换单元中定义,在每个转换单元中,内联函数或变量在被丢弃的语句之外被odr使用。

nszi6y05

nszi6y052#

在极少数情况下,违反网上解决可能是有益的。
例如,你可以使用std::aligned_storage<MyPimplType_sizeof, MyPimplType_alignof>来代替std::unique_ptr<MyPimplType>,并在MyPimplType类的构造函数/析构函数中测试真实的的sizeof和对齐。这称为aligned storage pimpl或类似的东西。当你想用impl-by-pointer(通过智能指针指向impl)替换impl-by-value(对齐存储而不是impl)时,这很有用。
接下来,可以创建一种新的对齐存储类型,它可以在构造函数/析构函数中自动测试sizeof和对齐:

private/my_aligned_storage_by_decl.hpppublic/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, ...);
}

这只能通过ODR违规来实现 * 并且如果公共和私有头不能合并为单个头,其中公共和私有头具有相同my_aligned_storage_by类的 *2个不同定义 *。

  • 实现方式:*

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

  • 用法示例:*
    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...
}

以下是这种方法的一些主要缺点:
1.你必须将包含my_aligned_storage作为成员的类的头文件也拆分为公共/私有头文件,并将公共头文件留给SDK,但将私有头文件包含在cpp文件中而不是公共头文件中。
1.您已经显式地控制了这些头的包含顺序,因为可以静默地包含私有头而不是公共头(但反之亦然)。
1.只有当类型变得完全完整时,才必须包含sizeof/alignment测试Assert的实现,这有时并不总是可能的。
1.你已经显式地指定了sizeof和alignment,它们在不同的上下文中可以是不同的,例如debug/release、windows/linux、msvc/gcc等等。
(*)如果my_aligned_storage的public和private header不能合并为一个public声明header。
这些缺点可以避免或忽略的情况下,对齐的用户类真的不是很大,经常构造/分配/复制,像一个内置类型。

相关问题