Подтвердить что ты не робот

Условно отключить конструктор копирования

Предположим, что я пишу шаблон класса C<T>, который содержит значение T, поэтому C<T> можно копировать, только если T можно копировать. Обычно, когда шаблон может поддерживать или не поддерживать определенную операцию, вы просто определяете операцию, и это зависит от ваших вызывающих абонентов, чтобы избежать ее вызова, когда она небезопасна:

template <typename T>
class C {
 private:
  T t;

 public:
  C(const C& rhs);
  C(C&& rhs);

  // other stuff
};

Однако это создает проблемы в случае конструктора копирования, потому что is_copy_constructible<C<T>> будет истинным, даже если T не может быть скопирован; черта не может видеть, что конструктор копирования будет плохо сформирован, если он вызвал. И это проблема, потому что, например, vector иногда избегает использования конструктора перемещения, если std::is_copy_constructible - true. Как я могу это исправить?

Я верю, что is_copy_constructible выполнит правильную работу, если конструктор явно или неявно дефолтован:

template <typename T>
class C {
 private:
  T t;

 public:
  C(const C& rhs) = default;
  C(C&& rhs) = default;

  // other stuff
};

Однако не всегда возможно структурировать ваш класс, чтобы конструкторы, построенные по умолчанию, поступили правильно.

Другим подходом, который я вижу, является использование SFINAE для условного отключения конструктора копирования:

template <typename T>
class C {
 private:
  T t;

 public:
  template <typename U = C>
  C(typename std::enable_if<std::is_copy_constructible<T>::value,
                            const U&>::type rhs);
  C(C&& rhs);

  // other stuff
};

Помимо уродства как греха, проблема с этим подходом заключается в том, что я должен сделать конструктор шаблоном, потому что SFINAE работает только с шаблонами. По определению конструкторы копирования не являются шаблонами, поэтому вещь, которую я отключу/разрешаю, на самом деле не является конструктором копирования, и, следовательно, она не будет подавлять конструктор копирования, который неявно предоставлен компилятором.

Я могу исправить это, явно удалив конструктор копирования:

template <typename T>
class C {
 private:
  T t;

 public:
  template <typename U = C>
  C(typename std::enable_if<std::is_copy_constructible<T>::value,
                            const U&>::type rhs);
  C(const C&) = delete;
  C(C&& rhs);

  // other stuff
};

Но это все еще не мешает рассмотрению конструктора копирования во время разрешения перегрузки. И эта проблема, потому что при прочих равных условиях обычная функция будет бить шаблон функции при разрешении перегрузки, поэтому, когда вы пытаетесь скопировать C<T>, выбирается обычный конструктор копирования, что приводит к сбою сборки, даже если T можно скопировать.

Единственный подход, который я могу найти, в принципе будет заключаться в том, чтобы опустить конструктор копирования из первичного шаблона и предоставить его в частичной специализации (используя больше обвинений SFINAE, чтобы отключить его, когда T не копируется). Тем не менее, это хрупкое, потому что для этого требуется, чтобы я дублировал все определение C, что создает серьезный риск того, что две копии будут выпадать из синхронизации. Я могу смягчить это, обладая тем, что тела методов совместно используют код, но мне все же приходится дублировать определения классов и списки элементов-конструкторов-членов, а также много места для ошибок. Я могу смягчить это, добавив, что они оба наследуют из общего базового класса, но введение наследования может иметь множество нежелательных последствий. Кроме того, публичное наследование просто кажется неправильным инструментом для работы, когда все, что я пытаюсь сделать, - отключить один конструктор.

Есть ли лучшие варианты, которые я не рассматривал?

4b9b3361

Ответ 1

Примечательным подходом является частичная специализация окружающего шаблона класса.

template <typename T,
          bool = std::is_copy_constructible<T>::value>
struct Foo
{
    T t;

    Foo() { /* ... */ }
    Foo(Foo const& other) : t(other.t) { /* ... */ }
};

template <typename T>
struct Foo<T, false> : Foo<T, true>
{
    using Foo<T, true>::Foo;

    // Now delete the copy constructor for this specialization:
    Foo(Foo const&) = delete;

    // These definitions adapt to what is provided in Foo<T, true>:
    Foo(Foo&&) = default;
    Foo& operator=(Foo&&) = default;
    Foo& operator=(Foo const&) = default;
};

Таким образом, черта is_copy_constructible выполняется точно там, где T is_copy_constructible.

Ответ 2

Однако не всегда возможно структурировать ваш класс, чтобы конструкторы, построенные по умолчанию, поступили правильно.

Это обычно возможно с достаточным усилием.

Делегировать работу, которая не может быть выполнена конструктором по умолчанию другому члену, или обернуть элемент T в некоторую оболочку, которая выполняет копирование, или переместить его в базовый класс, который определяет соответствующие операции.

Затем вы можете определить конструктор копирования как:

  C(const C&) = default;

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

template<bool copyable>
struct copyable_characteristic { };

template<>
struct copyable_characteristic<false> {
  copyable_characteristic() = default;
  copyable_characteristic(const copyable_characteristic&) = delete;
};

template <typename T>
class C
: copyable_characteristic<std::is_copy_constructible<T>::value>
{
 public:
  C(const C&) = default;
  C(C&& rhs);

  // other stuff
};

Это можно использовать для удаления операций с использованием произвольных условий, таких как is_nothrow_copy_constructible, а не просто спрямляемое T, которое можно скопировать, подразумевая, что C является скопируемым правилом.

Ответ 3

Если вы хотите условно отключить свой конструктор копирования, вы определенно хотите, чтобы он участвовал в разрешении перегрузки - потому что вы хотите, чтобы это была громкая ошибка компиляции, если вы попытаетесь ее скопировать.

И для этого вам нужно всего лишь static_assert:

template <typename T>
class C {
public:
    C(const C& rhs) {
        static_assert(some_requirement_on<T>::value, 
            "copying not supported for T");
    }
};

Это позволит построить копию только в том случае, если some_requirement_on<T> является истинным, и если оно ложно, вы все равно можете использовать остальную часть класса... просто не скопируйте конструкцию. И если вы это сделаете, вы получите ошибку компиляции, указывающую на эту строку.

Вот простой пример:

template <typename T>
struct Foo
{
    Foo() { }

    Foo(const Foo& ) {
        static_assert(std::is_integral<T>::value, "");
    }

    void print() {
        std::cout << "Hi" << std::endl;
    }
};

int main() {
    Foo<int> f;
    Foo<int> g(f); // OK, satisfies our condition
    g.print();     // prints Hi

    Foo<std::string> h;
    //Foo<std::string> j(h); // this line will not compile
    h.print(); // prints Hi
}

Ответ 4

C::C(C const& rhs, std::enable_if<true, int>::type dummy = 0) также является копией ctor, потому что второй аргумент имеет значение по умолчанию.

Ответ 5

Это трюк, но он работает.

template<bool b,class T>
struct block_if_helper{
  using type=T;
};
template<class T>
struct block_if_helper<true, T>{
  class type{
    type()=delete;
  };
};
template<bool b,classT>
using block_if=typename block_if_helper<b,T>::type;
template<bool b,classT>
using block_unless=typename block_if_helper<!b,T>::type;

теперь мы создаем метод, который является вашей копией ctor... возможно.

template<class X>
struct example {
  enum { can_copy = std::is_same<X,int>{} };

  example( block_unless<can_copy, example>const& o ); // implement this as if `o` was an `example`
  // = default not allowed
  example( block_if<can_copy, example>const& )=delete;
};

и теперь =default является копией ctor тогда и только тогда, когда can_copy, а =delete - нет. Тип заглушки, который иначе не может быть создан.

Я нахожу этот метод полезным для отключения общего метода в компиляторах, которые не поддерживают функцию аргумента шаблона по умолчанию, или для методов (например, virtual или специальных), которые не могут быть template s.

Ответ 6

template <typename T>
class variant {
    struct moo {};
public:
  variant(const variant& ) = default;
  variant(std::conditional_t<!std::is_copy_constructible<T>::value,
                             const variant&, moo>,
          moo=moo());
  variant() {};
};

Это приводит к тому, что экземпляр шаблона, не имеющий права доступа, имеет два конструктора копирования, что делает его невозможным для копирования.

Ответ 7

Стоит выкинуть новый ответ, поскольку это еще не упоминалось. Мы можем добавить базовый класс, который позволяет/отключает конструктор копирования для нас. Это локализует нужную функциональность:

template <bool >
struct enable_copy {
     enable_copy() = default;
     enable_copy(enable_copy const& ) = default;
     enable_copy& operator=(enable_copy const& ) = default;
};

template <>
struct enable_copy<false> {
     enable_copy() = default;
     enable_copy(enable_copy const& ) = delete;
     enable_copy& operator=(enable_copy const& ) = delete;
};

Теперь вам нужно только:

template <typename T>
struct Foo : enable_copy<std::is_copy_constructible<T>::value>
{
    Foo() = default;
    Foo(Foo const& ) = default;
};

Теперь Foo<T> сам будет скопирован конструктивным тогда и только тогда, когда T есть, но тело Foo прост.