Конфликт между конструктором копирования и конструктором пересылки

Эта проблема основана на коде, который работает у меня на GCC-4.6, но не работает у другого пользователя с CLang-3.0, оба в режиме C++0x.

template <typename T>
struct MyBase
{
//protected:
    T  m;

    template <typename Args...>
    MyBase( Args&& ...x ) : m( std::forward<Args>(x)... ) {}
};

Объект MyBase может принимать любой список аргументов конструктора, если T поддерживает эту сигнатуру конструкции. Проблема связана со специальными функциями-членами.

  1. IIUC, шаблон конструктора отменяет автоматически определенный конструктор по умолчанию. Однако, поскольку шаблон может принимать нулевые аргументы, он будет действовать как явно определенный конструктор по умолчанию (пока T можно конструировать по умолчанию).
  2. IIUC, определение политики создания копии класса игнорирует шаблоны конструктора. В данном случае это означает, что MyBase получит автоматически определяемый конструктор копирования (пока T можно копировать), который будет направлять T копирование-конструкцию.
  3. Примените предыдущий шаг и к конструкции перемещения.

Итак, если я передам MyBase<T> const & в качестве единственного аргумента конструктора, какой конструктор будет вызван, пересылающий или неявно копирующий?

typedef std::vector<Int>  int_vector;
typedef MyBase<int_vector>   VB_type;

int_vector  a{ 1, 3, 5 };
VB_type     b{ a };
VB_type     c{ b };  // which constructor gets called

Проблема моего пользователя заключалась в использовании этого в качестве базового класса. Компилятор пожаловался, что его класс не может синтезировать автоматически определяемый конструктор копии, потому что не может найти совпадение с шаблоном конструктора базового класса. Разве он не должен вызывать MyBase автоматический конструктор копирования для своего собственного автоматического конструктора копирования? Является ли CLang ошибкой из-за конфликта?


person CTMacUser    schedule 15.02.2012    source источник
comment
Интересный вопрос. Я рекомендую создать sscce (см. sscce.org ), который работает в GCC и не работает в CLang, чтобы помочь нам понять проблему. лучше и воссоздать его самостоятельно. Кроме того, пожалуйста, дайте нам точное сообщение об ошибке от CLang.   -  person David Grayson    schedule 15.02.2012
comment
Собственно, относительно близкая к голове версия gcc (по состоянию на 20120202 год) тоже не принимает этот код. Вроде конструктор пересылки подхватывается даже для копии. Я не совсем уверен, почему это. Это не связано с единым синтаксисом инициализации: даже при использовании круглых скобок для последнего объявления он не компилируется.   -  person Dietmar Kühl    schedule 15.02.2012
comment
Мой код — это то, что я написал десять лет назад для Boost и обновил для C++ 11 на прошлой неделе. Я не знаю, как работать со всей системой Boost Trac, но вы можете посмотреть мои изменения и текущее состояние файл (в ревизии 77031 на момент написания этой статьи).   -  person CTMacUser    schedule 15.02.2012
comment
Я нашел почтовый архив с описанием проблемы.   -  person CTMacUser    schedule 15.02.2012
comment
К вашему сведению, Скотт Мейерс обсуждал эту проблему в своем блоге по адресу scottmeyers.blogspot. ком/2012/10/   -  person haohaolee    schedule 25.10.2012


Ответы (3)


Я просто в баре с Ричардом Корденом, и между нами мы пришли к выводу, что проблема не имеет ничего общего с вариационным или rvalues. Неявно сгенерированная копирующая конструкция в этом случае принимает MyBase const& в качестве аргумента. Шаблонный конструктор вывел тип аргумента как MyBase&. Это лучшее совпадение, которое вызывается, хотя это не конструктор копирования.

Пример кода, который я использовал для тестирования, таков:

#include <utility>
#include <vector>i

template <typename T>
struct MyBase
{
    template <typename... S> MyBase(S&&... args):
        m(std::forward<S>(args)...)
    {
    }
    T m;
};

struct Derived: MyBase<std::vector<int> >
{
};

int main()
{
    std::vector<int>                vec(3, 1);
    MyBase<std::vector<int> > const fv1{ vec };
    MyBase<std::vector<int> >       fv2{ fv1 };
    MyBase<std::vector<int> >       fv3{ fv2 }; // ERROR!

    Derived d0;
    Derived d1(d0);
}

Мне нужно было удалить использование списков инициализаторов, потому что это пока не поддерживается clang. Этот пример компилируется, за исключением инициализации fv3, которая завершается ошибкой: конструктор копирования, синтезированный для MyBase<T>, принимает MyBase<T> const& и, таким образом, передает fv2, вызывает вариативный конструктор, пересылающий объект базовому классу.

Возможно, я неправильно понял вопрос, но на основе d0 и d1 кажется, что синтезируются как конструктор по умолчанию, так и конструктор копирования. Однако это с довольно современными версиями gcc и clang. То есть это не объясняет, почему конструктор копирования не синтезируется, потому что он синтезирован.

Чтобы подчеркнуть, что эта проблема не имеет ничего общего с вариативными списками аргументов или rvalue: следующий код показывает проблему, связанную с вызовом шаблонного конструктора, хотя это выглядит так, как будто вызывается конструктор копирования, а конструкторы копирования никогда не являются шаблонами. На самом деле это несколько удивительное поведение, о котором я определенно не знал:

#include <iostream>
struct MyBase
{
    MyBase() {}
    template <typename T> MyBase(T&) { std::cout << "template\n"; }
};

int main()
{
    MyBase f0;
    MyBase f1(const_cast<MyBase const&>(f0));
    MyBase f2(f0);
}

В результате добавление вариационного конструктора, как в вопросе, к классу, у которого нет других конструкторов, изменяет работу конструкторов копирования поведения! Лично я считаю, что это довольно печально. Фактически это означает, что класс MyBase необходимо также дополнить конструкторами копирования и перемещения:

    MyBase(MyBase const&) = default;
    MyBase(MyBase&) = default;
    MyBase(MyBase&&) = default;

К сожалению, это не работает с gcc: он жалуется на конструкторы копирования по умолчанию (он утверждает, что конструктор копирования по умолчанию, использующий неконстантную ссылку, не может быть определен в определении класса). Clang принимает этот код без каких-либо претензий. Использование определения конструктора копирования, использующего неконстантную ссылку, работает как с gcc, так и с clang:

template <typename T> MyBase<T>::MyBase(MyBase<T>&) = default;
person Dietmar Kühl    schedule 15.02.2012
comment
@MooingDuck: я не смог воспроизвести проблему с использованием соответствующего базового класса (см. мое обновление к ответу). - person Dietmar Kühl; 15.02.2012
comment
Да, изначально я использовал разные имена и не обновлял все места. Надеюсь, теперь я все исправил. - person Dietmar Kühl; 16.02.2012

У меня лично была проблема со снимками GCC уже довольно давно. У меня были проблемы с пониманием того, что происходит (и разрешено ли это вообще), но я пришел к тому же выводу, что и Дитмар Кюль: конструкторы копирования/перемещения все еще здесь, но не всегда предпочтительнее из-за механики перегрузки. разрешающая способность.

Я использовал это, чтобы обойти проблему в течение некоторого времени:

// I don't use std::decay on purpose but it shouldn't matter
template<typename T, typename U>
using is_related = std::is_same<
    typename std::remove_cv<typename std::remove_reference<T>::type>::type
    , typename std::remove_cv<typename std::remove_reference<U>::type>::type
>;

template<typename... T>
struct enable_if_unrelated: std::enable_if<true> {};

template<typename T, typename U, typename... Us>
struct enable_if_unrelated
: std::enable_if<!is_related<T, U>::value> {};

Использование его с конструктором, подобным вашему, будет выглядеть так:

template<
    typename... Args
    , typename = typename enable_if_unrelated<MyBase, Args...>::type
>
MyBase(Args&&... args);

Некоторые пояснения в порядке. is_related — это стандартная бинарная черта, которая проверяет идентичность двух типов независимо от спецификаторов верхнего уровня (const, volatile, &, &&). Идея состоит в том, что конструкторы, которые будут защищены этим трейтом, являются «конвертирующими» конструкторами и не предназначены для работы с параметрами самого типа класса, но только если этот параметр находится в первой позиции. Конструкция с параметрами, например. (std::allocator_arg_t, MyBase) было бы хорошо.

Раньше у меня тоже была enable_if_unrelated в качестве бинарной метафункции, но, поскольку очень удобно, чтобы идеально пересылающие вариативные конструкторы работали и в нульарном случае, я переделал ее так, чтобы она принимала любое количество аргументов (хотя ее можно было бы спроектировать так, чтобы она принимала как минимум один аргумент, тип класса конструктора, который мы охраняем). Это означает, что в нашем случае, если конструктор вызывается без аргументов, он не выводится SFINAE. В противном случае вам нужно будет добавить объявление MyBase() = default;.

Наконец, если конструктор пересылает в базу, другой альтернативой является наследование конструктора этой базы (т.е. using Base::Base;). В вашем примере это не так.

person Luc Danton    schedule 15.02.2012

Я проголосовал за ответ Дитмара, потому что полностью с ним согласен. Но я хочу поделиться «решением», которое я использовал некоторое время назад, чтобы избежать этих проблем:

Я намеренно добавил фиктивный параметр в вариативный конструктор:

enum fwd_t {fwd};

template<class T>
class wrapper
{
    T m;
public:
    template<class...Args>
    wrapper(fwd_t, Args&&...args)
    : m(std::forward<Args>(args)...)
    {}
};

:::

int main()
{
    wrapper<std::string> w (fwd,"hello world");
}

Тем более, что конструктор может принять что угодно без этого фиктивного параметра, кажется целесообразным, чтобы пользовательский код явно выбирал правильный конструктор, (своего рода) «называя» его.

В вашем случае это может быть невозможно. Но иногда можно обойтись без него.

person sellibitze    schedule 16.02.2012