Нарушение ODR в GCC 6.3.0 с типами, определенными в двух отдельных единицах трансляции

В следующем примере кода мы наблюдаем странное поведение в GCC. Странным поведением является нарушение ODR в GCC 6.3.0 с типами, определенными в двух отдельных единицах трансляции. Возможно, это связано с определениями рекурсивных типов или неполными типами.

Мы не уверены, действителен ли наш код или мы зависим от неопределенного поведения в способе рекурсивного определения наших типов. Пожалуйста, посмотрите, как вариантный шаблон динамического класса определяется и создается в двух отдельных файлах 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 

Странно, что построение вектора приводит нас прямо к деструктору.

Что очень странно, эти ошибки исчезают, когда мы делаем следующее:

  1. Переименуйте «Типы» в одном из файлов cpp, чтобы они не использовали одно и то же имя для шаблона класса.
  2. Сделайте реализацию «Типов» одинаковой в каждом файле cpp (измените Number на удвоение в каждом файле).
  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>;
};

Мы также видим некоторые предупреждения, связанные с нарушением ODR, когда мы компилируем с оптимизацией времени компоновки (LTO):

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

Есть ли у кого-нибудь представление о том, что может вызывать эту проблему?


person ethortsen    schedule 05.04.2017    source источник
comment
Похоже, это могло быть внутреннее обращение с ними, как если бы они были одного типа; если да, то, вероятно, это ошибка компилятора, но я не уверен на 100%, что об этом говорится в стандарте. Только предположение, однако, у меня не установлен GCC, а единственная известная мне онлайн-среда GCC, которая позволяет использовать несколько файлов, устарела.   -  person Justin Time - Reinstate Monica    schedule 05.04.2017
comment
Устраняют ли проблему два пространства имен с разными именами в исходных файлах (вместо анонимных пространств имен)?   -  person Mark B    schedule 05.04.2017
comment
Если вы измените порядок файлов dynamic_test.cpp в командной строке, изменится ли это, из какого теста исходит segfault?   -  person Nir Friedman    schedule 05.04.2017
comment
Что вам также следует сделать, так это предоставить команды компиляции и связывания, которые вы используете здесь, чтобы завершить ваш репро-случай. Также попробуйте разделить связывание и компиляцию на отдельные шаги. После того, как вы скомпилировали файлы .o, вы должны запустить nm для них. nm должен отображать символ Types в нижнем регистре t.   -  person Nir Friedman    schedule 05.04.2017
comment
@JustinTime Спасибо, это тоже наше подозрение, но также любопытно, являются ли неполные типы фактором. Мы обнаружили аналогичную ошибку, опубликованную в GCC Bugzilla: gcc.gnu.org/bugzilla/ show_bug.cgi? id = 70413   -  person ethortsen    schedule 05.04.2017
comment
@MarkB Да, другое название пространства имен также решает проблему.   -  person ethortsen    schedule 05.04.2017
comment
@NirFriedman Да, изменение порядка в командной строке действительно меняет тест, из которого исходит segfault. Кроме того, спасибо за предложение, я попробую.   -  person ethortsen    schedule 05.04.2017
comment
@ethortsen При изменении порядка изменения, который вызывает segfault, тест, связанный со второй связью, является segfault, наиболее вероятно, что определение Types из первой ссылки используется во второй ссылке. Я бы не ожидал, что это произойдет с чем-то, что имеет внутреннюю связь, поэтому я спросил о nm. Я пытался сделать репро локально, но мне это не удалось (другая ОС, другой gcc, кто знает). Вы можете попробовать определить статическую строку для каждого Types по-разному и распечатать ее как часть теста, чтобы попытаться доказать это.   -  person Nir Friedman    schedule 06.04.2017


Ответы (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<Foo> с внешней связью. Вторая единица перевода делает то же самое. Поэтому, когда компоновщик связывает их, он видит две единицы перевода, пытающиеся определить один и тот же символ с внешней связью. Обратите внимание, что это встроенный член класса, поэтому он неявно встроен. Таким образом, компоновщик обрабатывает это, как и всегда: он просто молча отбрасывает все определения, с которыми он сталкивается после первого (потому что символ уже определен; так работает компоновщик слева направо). Итак, Foo правильно определен в каждом TU , но Bar<Foo> нет.

Суть в том, что это нарушение ODR. Вы захотите переосмыслить некоторые вещи.

Изменить: похоже, что это ошибка в gcc. Формулировка стандарта подразумевает, что Foo должны обрабатываться однозначно в этой ситуации, и, следовательно, Bar шаблон для каждого Foo должен быть отдельным. Ссылка на ошибку: https://gcc.gnu.org/bugzilla/show_bug.cgi?id=70413.

person Nir Friedman    schedule 05.04.2017
comment
Между тем, MSVC с радостью делает именно то, что вы хотите, потому что компилятор дает каждому анонимному пространству имен свое собственное уникальное внутреннее имя и, таким образом, видит каждое Bar<Foo> как отдельную сущность. (Например, при компиляции с VS2010 (после изменения typedefs с using на typedef, потому что это было быстрее, чем переход к компьютеру с установленным VS2015), он генерирует ?foo@?$Bar@UFoo@?A0x67d7c1f5@@@@SAHXZ для test1.cpp и ?foo@?$Bar@UFoo@?A0xe143b35b@@@@SAHXZ для test2.cpp., Где каждое A<hex number> является анонимным пространством имен. ) - person Justin Time - Reinstate Monica; 06.04.2017
comment
Мне любопытно, слишком ли снисходителен MSVC к анонимным пространствам имен здесь, или у GCC есть проблемы с различением анонимных пространств имен. - person Justin Time - Reinstate Monica; 06.04.2017
comment
@ Justin Time Я не совсем понимаю, что вы имеете в виду под слишком снисходительным. Я сомневаюсь, что это вообще предусмотрено стандартом. Большинство компоновщиков - нет. Но счастлив, что ошибся. Я встречал много примеров, когда компоновщик msvcs ведет себя лучше. В Linux забавно легко вызвать segfaults с правильным кодом, в котором смешаны глобальные, статические и динамические ссылки. - person Nir Friedman; 06.04.2017
comment
Под «слишком снисходительным» я имел в виду, что если это было нарушение ODR, то MSVC не позволил бы ему компилироваться; Я не был уверен, было ли это нарушение ODR или ошибка, поэтому я не знал, какой компилятор виноват. - person Justin Time - Reinstate Monica; 06.04.2017
comment
Я только что взглянул на стандарт, и [namespace.unnamed/1] говорит, что каждое безымянное пространство имен должно обрабатываться так, как будто оно имеет уникальное имя, и что хотя сущности в безымянном пространстве имен могут иметь внешнюю связь, они эффективно квалифицируются имя уникально для их единицы перевода и поэтому не может быть замечено ни в одной другой единице перевода. Возможно, я неправильно интерпретирую, но я считаю, что это ошибка GCC. - person Justin Time - Reinstate Monica; 06.04.2017
comment
(И да, я заметил, что компоновщик MSVC также более надежен. Я лично считаю, что большая часть этого заключается в том, что их схема изменения имени более тщательная, чем у GCC, особенно в том, что касается глобальных переменных и возвращаемых типов. Это много сложнее сломать случайно или намеренно, потому что он сохраняет в основном все объявление.) - person Justin Time - Reinstate Monica; 06.04.2017
comment
@JustinTime Вы хорошо заметили эту стандартную цитату, она становится довольно неоднозначной, что касается шаблонов. Я могу спросить об этом отдельно. Что касается MSVC, мой самый большой пример не связан с изменением имен. Двойное удаление глобалов в Linux без уважительной причины. - person Nir Friedman; 06.04.2017
comment
Просто хочу сказать, спасибо за все отзывы и помощь. @NirFriedman, ваш пример, кажется, раскрывает суть проблемы и очень похож на пример, представленный на GCC Bugzilla, где похоже, что ошибка была недавно подтверждена (gcc.gnu.org/bugzilla/show_bug.cgi?id=70413). Однако очень интересно попытаться понять, что в данном случае означает стандарт. - person ethortsen; 06.04.2017
comment
@ethortsen С удовольствием, это интересная проблема. Я отредактировал свой ответ, включив основные моменты беседы в комментарии и ссылку на ошибку. Я не уверен, что можно еще что-то сказать, поскольку я не вижу, чтобы кто-либо возражал против того, чтобы это было ошибкой. Сообщите мне, есть ли что-нибудь еще, чтобы вы могли отметить это как принятый. - person Nir Friedman; 07.04.2017
comment
@NirFriedman Я бы предположил, что уникальность должна сохраняться даже тогда, когда члены анонимного пространства имен используются в качестве параметров шаблона, чтобы предотвратить нарушения ODR, но это можно было бы сформулировать более четко. Я не знал, что у компоновщика GCC были проблемы с двойным удалением глобальных объектов, хотя это довольно странно. - person Justin Time - Reinstate Monica; 07.04.2017