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

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

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

我们不确定我们的代码是否有效,或者我们是否以递归定义类型的方式依赖于未定义的行为。请查看类似变体的 Dynamic class 模板是如何在两个单独的 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 ) );
}

main.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 文件中的 "Types",这样它们就不会使用 class 模板同名。
  2. 使 "Types" 的实现在每个 cpp 文件中相同(更改 每个文件中要加倍的数字)。
  3. 不要将数字推送到矢量。
  4. 更改 Dynamic 的实现以不使用此递归类型 定义样式。

这是一个确实有效的实施示例:

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>;
};

当我们使用 link 时间优化 (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 returns 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*)

暂时不要担心字母,除了大写字母与小写字母。小写符号是私有的,就像我们期望的内部链接符号一样。大写符号是 public,具有外部链接并暴露给链接器。

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

最重要的是,这是 ODR 违规行为。你会想重新考虑一些事情。

编辑:这似乎实际上是 gcc 中的错误。该标准的措辞暗示在这种情况下应该对 Foo 进行唯一处理,因此在每个 Foo 上模板化的 Bar 应该是分开的。 Link 错误:https://gcc.gnu.org/bugzilla/show_bug.cgi?id=70413.