Предположим, что я пишу шаблон класса 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
, что создает серьезный риск того, что две копии будут выпадать из синхронизации. Я могу смягчить это, обладая тем, что тела методов совместно используют код, но мне все же приходится дублировать определения классов и списки элементов-конструкторов-членов, а также много места для ошибок. Я могу смягчить это, добавив, что они оба наследуют из общего базового класса, но введение наследования может иметь множество нежелательных последствий. Кроме того, публичное наследование просто кажется неправильным инструментом для работы, когда все, что я пытаюсь сделать, - отключить один конструктор.
Есть ли лучшие варианты, которые я не рассматривал?