【问题标题】:ODR violation in GCC 6.3.0 with types defined in two separate translation unitsGCC 6.3.0 中的 ODR 违规,类型在两个单独的翻译单元中定义
【发布时间】:2023-04-05 19:26:01
【问题描述】:

通过以下代码示例,我们在 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 ) );
}

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 文件中重命名“类型”,这样它们就不会使用 类模板的名称相同。
  2. 在每个 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>;
};

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

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

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

【问题讨论】:

  • 看起来它可能在内部将它们视为同一类型;如果是这样,它可能是编译器错误,但我不能 100% 确定标准对此有何规定。不过,只是猜测,我没有安装 GCC,而且我所知道的唯一允许多个文件的在线 GCC 环境已经过时。
  • 源文件中的两个不同命名的命名空间(而不是匿名命名空间)是否也能解决问题?
  • 如果在命令行中改变dynamic_test.cpp文件的顺序,是否会改变segfault来自哪个测试?
  • 您还应该提供您在此处使用的编译和链接命令,以使您的复制案例完整。还要尝试将链接和编译分离为单独的步骤。编译 .o 文件后,您应该在它们上运行 nmnm 应显示符号 Types 和小写字母 t。
  • @ethortsen 如果更改排序更改哪个测试导致段错误,第二个链接的测试是段错误,最有可能发生的是第一个链接的 Types 的定义是在第二个链接中使用。我不希望这种情况发生在具有内部联系的事情上,这就是我询问 nm 的原因。我试图在本地进行复制,但失败了(不同的操作系统,不同的 gcc,谁知道)。您可以尝试为每个 Types 不同地定义一个静态字符串,并将其打印为测试的一部分以尝试证明它。

标签: c++ gcc g++ one-definition-rule


【解决方案1】:

好的,我花了一些时间来打开和关闭它,但我终于得到了一个非常简单的副本,它抓住了问题的核心。首先考虑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&lt;Foo&gt;。第二个翻译单元做同样的事情。因此,当链接器链接它们时,它会看到两个翻译单元试图用外部链接定义相同的符号。请注意,它是内联定义的类成员,因此它是隐式内联的。所以链接器会像往常一样处理这个问题:它只是默默地删除它在第一个定义之后遇到的所有定义(因为符号已经定义;这就是链接器的工作方式,从左到右)。所以Foo 在每个 TU,但 Bar&lt;Foo&gt; 不是。

底线是这是违反 ODR 的。你会想重新考虑一些东西。

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

【讨论】:

  • 同时,MSVC 可以满足您的要求,因为编译器为每个匿名命名空间提供了自己唯一的内部名称,因此它将每个 Bar&lt;Foo&gt; 视为一个单独的实体。 (例如,用 VS2010 编译(将 typedef 从 using 更改为 typedef 后,因为它比走到安装了 VS2015 的计算机上更快),它为 test1.cpp 生成 ?foo@?$Bar@UFoo@?A0x67d7c1f5@@@@SAHXZ 和为 @ 生成 ?foo@?$Bar@UFoo@?A0xe143b35b@@@@SAHXZ 987654354@.,其中每个A&lt;hex number&gt; 是一个匿名命名空间。)
  • 我很好奇 MSVC 是否对匿名命名空间过于宽容,或者 GCC 是否难以区分匿名命名空间。
  • @Justin Time 我不太确定您所说的过于宽容是什么意思。我怀疑标准是否涵盖了这一点。大多数链接器行为不是。不过很高兴被证明是错误的。我遇到过许多 msvcs 链接器表现更好的示例。在 Linux 上,使用混合了全局、静态和动态链接的有效代码很容易导致段错误。
  • “过于宽松”的意思是,如果它违反了 ODR,那么 MSVC 让它编译是错误的;我不确定这是违反 ODR 还是错误,这就是为什么我不知道哪个编译器出了问题。
  • 只想说,感谢到目前为止的所有反馈和帮助。 @NirFriedman,您的示例似乎触及了问题的症结所在,并且看起来很像 GCC Bugzilla 上提供的示例,其中似乎最近确认了该错误 (gcc.gnu.org/bugzilla/show_bug.cgi?id=70413)。但是,尝试了解该标准在这种情况下的意图是非常有趣的。
猜你喜欢
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 2015-03-01
相关资源
最近更新 更多