c++ - qué - que son los constructores en c
Deshabilitar condicionalmente un constructor de copia (7)
Supongamos que estoy escribiendo una plantilla de clase C<T>
que contiene un valor T
, por lo que C<T>
se puede copiar si T
es copiable. Normalmente, cuando una plantilla puede o no ser compatible con una determinada operación, usted solo define la operación, y depende de las personas que llaman evitar llamarla cuando no sea segura:
template <typename T>
class C {
private:
T t;
public:
C(const C& rhs);
C(C&& rhs);
// other stuff
};
Sin embargo, esto crea problemas en el caso de un constructor de copia, porque is_copy_constructible<C<T>>
será verdadero incluso cuando T
no se puede copiar; el rasgo no puede ver que el constructor de copia estará mal formado si se lo llama. Y eso es un problema porque, por ejemplo, vector
a veces evitará usar el constructor de movimientos si std::is_copy_constructible
es verdadero. ¿Cómo puedo arreglar esto?
Creo que is_copy_constructible
hará lo correcto si el constructor está explícita o implícitamente predeterminado:
template <typename T>
class C {
private:
T t;
public:
C(const C& rhs) = default;
C(C&& rhs) = default;
// other stuff
};
Sin embargo, no siempre es posible estructurar su clase para que los constructores en incumplimiento hagan lo correcto.
El otro enfoque que puedo ver es usar SFINAE para deshabilitar condicionalmente el constructor de copias:
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
};
Además de ser feo como el pecado, el problema con este enfoque es que tengo que hacer que el constructor sea una plantilla, porque SFINAE solo funciona en plantillas. Por definición, los constructores de copia no son plantillas, así que lo que estoy deshabilitando / habilitando no es en realidad el constructor de copia, y en consecuencia no suprimirá el constructor de copia que es provisto implícitamente por el compilador.
Puedo solucionar esto eliminando explícitamente el constructor de copia:
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
};
Pero eso aún no impide que el constructor de copia sea considerado durante la resolución de sobrecarga. Y eso es un problema porque todo lo demás es igual, una función normal vencerá una plantilla de función en resolución de sobrecarga, por lo que cuando intente copiar un C<T>
, el constructor de copia ordinaria se seleccionará, lo que generará un error de compilación incluso si T
copiable
El único enfoque que puedo encontrar que en principio funcionará es omitir el constructor de copia de la plantilla primaria, y proporcionarlo en una especialización parcial (usando más trucos de SFINAE para deshabilitarlo cuando T no se puede copiar). Sin embargo, esto es frágil, porque me exige duplicar toda la definición de C
, lo que crea un riesgo importante de que las dos copias no estén sincronizadas. Puedo mitigar esto haciendo que los cuerpos de método compartan el código, pero aún tengo que duplicar las definiciones de clase y las listas de miembros de inicio del constructor, y eso deja mucho espacio para que los errores entren sigilosamente. Puedo mitigar esto aún más haciendo que ambos hereden de una clase base común, pero la introducción de la herencia puede tener una variedad de consecuencias no deseadas. Además, la herencia pública simplemente parece una herramienta incorrecta para el trabajo cuando lo único que intento hacer es desactivar un constructor.
¿Hay mejores opciones que no he considerado?
Sin embargo, no siempre es posible estructurar su clase para que los constructores en incumplimiento hagan lo correcto.
Usualmente es posible con suficiente esfuerzo.
Delegue el trabajo que no puede realizar un constructor predeterminado a otro miembro, o envuelva al miembro T
en algún contenedor que realice la copia, o muévalo a una clase base que defina las operaciones relevantes.
Luego puede definir el constructor de copia como:
C(const C&) = default;
Otra forma de hacer que el compilador decida si la definición predeterminada debe eliminarse o no es a través de una clase base:
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
};
Esto se puede usar para eliminar operaciones usando condiciones arbitrarias, como is_nothrow_copy_constructible
lugar de simplemente is_nothrow_copy_constructible
una T directa. C implica que se puede copiar una regla.
Esto es un truco, pero funciona.
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;
ahora creamos un método que es tu copiador ... tal vez.
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;
};
y ahora el =default
es el ctor de copiado si y solo si can_copy
, y el =delete
de no. El tipo de stub que es de otra manera no se puede crear.
Encuentro que esta técnica es útil para la desactivación de métodos generales en compiladores que no admiten la característica de argumento de plantilla predeterminada, o para métodos (como virtual
o especiales) que no pueden ser template
s.
Si desea deshabilitar de forma condicional su constructor de copia, definitivamente desea que participe en la resolución de sobrecarga, porque desea que sea un gran error de compilación si intenta copiarlo.
Y para hacer eso, todo lo que necesitas es static_assert
:
template <typename T>
class C {
public:
C(const C& rhs) {
static_assert(some_requirement_on<T>::value,
"copying not supported for T");
}
};
Esto permitirá la construcción de copias solo si some_requirement_on<T>
es verdadero, y si es falso, puede seguir usando el resto de la clase ... simplemente no copie la construcción. Y si lo haces, obtendrás un error de compilación apuntando a esta línea.
Aquí hay un ejemplo simple:
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
}
Un enfoque digno de mención es la especialización parcial de la plantilla de clase circundante.
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;
};
De esta forma, el rasgo is_copy_constructible
se cumple exactamente donde T
is_copy_constructible
.
Vale la pena arrojar una nueva respuesta ya que esto no se ha mencionado aún. Podemos agregar una clase base que habilite / deshabilite el constructor de copia para nosotros. Esto localiza la funcionalidad que queremos:
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;
};
Y ahora todo lo que necesitas es:
template <typename T>
struct Foo : enable_copy<std::is_copy_constructible<T>::value>
{
Foo() = default;
Foo(Foo const& ) = default;
};
Ahora, Foo<T>
mismo será copiable y construible si y solo si T
es, pero el cuerpo de Foo
es simple.
C::C(C const& rhs, std::enable_if<true, int>::type dummy = 0)
también es un copiador porque el segundo argumento tiene un valor predeterminado.
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() {};
};
Esto hace que una instancia de plantilla no elegible tenga dos constructores de copia, lo que hace que no sea copiable.