GCC 6.3.0 中的 ODR 冲突,类型在两个单独的翻译单元中定义

ODR violation in GCC 6.3.0 with types defined in two separate translation units

本文关键字:单独 两个 翻译 定义 单元 中的 ODR 冲突 类型 GCC      更新时间:2023-10-16

我们在下面的代码示例中看到了GCC中的一些奇怪的行为。奇怪的行为是 GCC 6.3.0 中的 ODR 违规,其类型在两个单独的翻译单元中定义。它可能与递归类型定义或不完整的类型有关。

我们不确定我们的代码是否有效,或者我们是否依赖于递归定义类型的未定义行为。请参阅如何在两个单独的 cpp 文件中定义和实例化类似变体的动态类模板。

dynamic_test.h:

#pragma once
#include <algorithm>
#include <type_traits>
namespace dynamic
{
template <class T>
void erasure_destroy( const void *p )
{
reinterpret_cast<const T*>( p )->~T();
}
template <class T>
void erasure_copy( void *pDest, const void *pSrc )
{
::new( pDest ) T( *reinterpret_cast<const T*>( pSrc ) );
}
template <class T>
struct TypeArg {};
struct ErasureFuncs
{
template <class T = ErasureFuncs>
ErasureFuncs( TypeArg<T> t = TypeArg<T>() ) :
pDestroy( &erasure_destroy<T> ),
pCopy( &erasure_copy<T> )
{
(void)t;
}
std::add_pointer_t<void( const void* )> pDestroy;
std::add_pointer_t<void( void*, const void* )> pCopy;
};
enum class TypeValue
{
Null,
Number,
Vector
};
template <typename T>
using unqual = std::remove_cv_t<std::remove_reference_t<T>>;
template <class Base, class Derived>
using disable_if_same_or_derived = std::enable_if_t<!std::is_base_of<Base, unqual<Derived>>::value>;
template <template <class> class TypesT>
struct Dynamic
{
using Types = TypesT<Dynamic>;
using Null = typename Types::Null;
using Number = typename Types::Number;
using Vector = typename Types::Vector;
Dynamic()
{
construct<Null>( nullptr );
}
~Dynamic()
{
m_erasureFuncs.pDestroy( &m_data );
}
Dynamic( const Dynamic &d ) :
m_typeValue( d.m_typeValue ),
m_erasureFuncs( d.m_erasureFuncs )
{
m_erasureFuncs.pCopy( &m_data, &d.m_data );
}
Dynamic( Dynamic &&d ) = delete;
template <class T, class = disable_if_same_or_derived<Dynamic, T>>
Dynamic( T &&value )
{
construct<unqual<T>>( std::forward<T>( value ) );
}
Dynamic &operator=( const Dynamic &d ) = delete;
Dynamic &operator=( Dynamic &&d ) = delete;
private:
static TypeValue to_type_value( TypeArg<Null> )
{
return TypeValue::Null;
}
static TypeValue to_type_value( TypeArg<Number> )
{
return TypeValue::Number;
}
static TypeValue to_type_value( TypeArg<Vector> )
{
return TypeValue::Vector;
}
template <class T, class...Args>
void construct( Args&&...args )
{
m_typeValue = to_type_value( TypeArg<T>() );
m_erasureFuncs = TypeArg<T>();
new ( &m_data ) T( std::forward<Args>( args )... );
}
private:
TypeValue m_typeValue;
ErasureFuncs m_erasureFuncs;
std::aligned_union_t<0, Null, Number, Vector> m_data;
};
}
void test1();
void test2();

dynamic_test_1.cpp:

#include "dynamic_test.h"
#include <vector>
namespace
{
template <class DynamicType>
struct Types
{
using Null = std::nullptr_t;
using Number = long double;
using Vector = std::vector<DynamicType>;
};
using D = dynamic::Dynamic<Types>;
}
void test1()
{
D::Vector v1;
v1.emplace_back( D::Number( 0 ) );
}

dynamic_test_2.cpp:

#include "dynamic_test.h"
#include <vector>
namespace
{
template <class DynamicType>
struct Types
{
using Null = std::nullptr_t;
using Number = double;
using Vector = std::vector<DynamicType>;
};
using D = dynamic::Dynamic<Types>;
}
void test2()
{
D::Vector v1;
v1.emplace_back( D::Number( 0 ) );
}

主.cpp:

#include "dynamic_test.h"
int main( int, char* const [] )
{
test1();
test2();
return 0;
}

运行此代码会导致具有以下堆栈跟踪的 SIGSEGV:

1 ??                                                                                                                                     0x1fa51  
2 dynamic::Dynamic<(anonymous namespace)::Types>::~Dynamic                                                        dynamic_test.h     66  0x40152b 
3 std::_Destroy<dynamic::Dynamic<(anonymous namespace)::Types>>                                                   stl_construct.h    93  0x4013c1 
4 std::_Destroy_aux<false>::__destroy<dynamic::Dynamic<(anonymous namespace)::Types> *>                           stl_construct.h    103 0x40126b 
5 std::_Destroy<dynamic::Dynamic<(anonymous namespace)::Types> *>                                                 stl_construct.h    126 0x400fa8 
6 std::_Destroy<dynamic::Dynamic<(anonymous namespace)::Types> *, dynamic::Dynamic<(anonymous namespace)::Types>> stl_construct.h    151 0x400cd1 
7 std::vector<dynamic::Dynamic<(anonymous namespace)::Types>>::~vector                                            stl_vector.h       426 0x400b75 
8 test2                                                                                                           dynamic_test_2.cpp 20  0x401796 
9 main                                                                                                            main.cpp           6   0x400a9f 

奇怪的是,构造一个 Vector 会把我们直接带到析构函数。

非常奇怪的是,当我们执行以下操作时,这些错误就会消失:

  1. 重命名其中一个 cpp 文件中的"类型",以便它们不使用 类模板的名称相同。
  2. 使每个 cpp 文件中"类型"的实现相同(更改 每个文件中的数字要加倍)。
  3. 不要将数字推送到向量。
  4. 将动态的实现更改为不使用此递归类型 定义样式。

下面是一个可以工作的实现的精简示例:

template <class Types>
struct Dynamic
{
using Null = typename Types::Null;
using Number = typename Types::Number;
using Vector = typename Types::template Vector<Dynamic>;
...
struct Types
{
using Null = std::nullptr_t;
using Number = long double;
template <class DynamicType>
using Vector = std::vector<DynamicType>;
};

当我们使用链接时间优化 (LTO) 进行编译时,我们还会看到一些与 ODR 违规相关的警告:

dynamic_test.h:51: warning: type ‘struct Dynamic’ violates the C++ One Definition Rule [-Wodr]
struct Dynamic
^

有没有人对可能导致此问题的原因有所了解?

好吧,我花了一段时间断断续续地玩这个,但我终于得到了一个非常简单的副本,它抓住了问题的核心。首先,考虑test1.cpp

#include "header.h"
#include <iostream>
namespace {
template <class T>
struct Foo {
static int foo() { return 1; };
};
using D = Bar<Foo>;
}
void test1() {
std::cerr << "Test1: " << D::foo() << "n";
}

现在,test2.cpp与此完全相同,只是Foo::foo返回 2,并且底部声明的函数称为test2并打印Test2:等等。接下来,header.h

template <template <class> class TT>
struct Bar {
using type = TT<Bar>;
static int foo() { return type::foo(); }
};

void test1();
void test2();

最后,main.x.cpp

#include "header.h"
int main() {
test1();
test2();
return 0;
}

您可能会惊讶地发现该程序打印:

Test1: 1
Test2: 1

当然,这只是因为我编译:

g++ -std=c++14 main.x.cpp test1.cpp test2.cpp

如果我颠倒最后两个文件的顺序,它们都会打印 2。

正在发生的事情是,链接器最终在需要它的地方使用它遇到的Foo的第一个定义。嗯,但我们在匿名命名空间中定义了Foo,这应该为其提供内部链接,从而避免此问题。所以我们只编译一个 TU,然后在上面使用nm

g++ -std=c++14 -c test1.cpp
nm -C test1.o

这将产生以下结果:

U __cxa_atexit
U __dso_handle
0000000000000087 t _GLOBAL__sub_I__Z5test1v
0000000000000049 t __static_initialization_and_destruction_0(int, int)
0000000000000000 T test1()
000000000000003e t (anonymous namespace)::Foo<Bar<(anonymous namespace)::Foo> >::foo()
0000000000000000 W Bar<(anonymous namespace)::Foo>::foo()
U std::ostream::operator<<(int)
U std::ios_base::Init::Init()
U std::ios_base::Init::~Init()
U std::cerr
0000000000000000 r std::piecewise_construct
0000000000000000 b std::__ioinit
U std::basic_ostream<char, std::char_traits<char> >& std::operator<< <std::char_traits<char> >(std::basic_ostream<char, std::char_traits<char> >&, char const*)

暂时不要担心字母,除了大写与小写。小写符号是私有的,我们期望内部链接符号的方式。大写符号是公共的,具有外部链接,并向链接器公开。

有趣的是,虽然Foo可能有内部联系,但Bar没有!第一个翻译单元已经定义了带有外部链接的符号Bar<Foo>。第二个翻译单元也做同样的事情。因此,当链接器链接它们时,它会看到两个翻译单元试图使用外部链接定义相同的符号。请注意,它是一个内联定义的类成员,因此它是隐式内联的。因此,链接器像往常一样处理这个问题:它只是静默地删除它在第一个之后遇到的所有定义(因为符号已经定义;这就是链接器的工作方式,从左到右)。因此,Foo在每个 TU 中都正确定义,但Bar<Foo>没有。

最重要的是,这是违反 ODR 的行为。你会想重新考虑一些东西。

编辑:实际上这似乎是 gcc 中的一个错误。该标准的措辞意味着在这种情况下,Foo应该被独特地对待,因此每个Foo上模板化的Bar应该是分开的。链接到错误:https://gcc.gnu.org/bugzilla/show_bug.cgi?id=70413。