plusplus - c++ website
¿Qué significa realmente "Asignable"? (2)
En C ++ 03, Assignable
se definió en la tabla 64 en §23.1 / 4,
Expression Return type Post-condition t = u T& t is equivalent to u
Por un lado, este requisito no se cumplió para std::map
. Por otro lado, era un requisito demasiado estricto para std::list
. Y C ++ 11 demostró que ni siquiera es necesario para std::vector
, en general, sino que se impone mediante el uso de ciertas operaciones (como la asignación).
En C ++ 11, el requisito correspondiente se llama CopyAssignable
y se define en la tabla 23 en §17.6.3.1 / 2,
Expression Return type Return value Post-condition t = v T& t t is equivalent to v, the value of v is unchanged
Las principales diferencias son que los elementos de contenedor ya no necesitan ser CopyAssignable
, y que hay un requisito correspondiente MoveAssignable
.
De todos modos, una estructura con un miembro de datos const
claramente no es asignable a menos que uno elija leer "equivalente a" con una interpretación muy peculiar.
El único requisito de tipo de elemento independiente de la operación en C ++ 11 es, por lo que puedo ver (de la tabla 96 en §23.2.1 / 4) que debe ser Destructible
.
Con respecto a std::is_assignable
, no prueba el criterio CopyAssignable
.
std::is_assignable<T, U>
es lo que std::is_assignable<T, U>
, según la tabla 49 en C ++ 11 §20.9.4.3 / 3:
“La expresión
declval<T>() = declval<U>()
está bien formada cuando se trata como un operando sin evaluar (Cláusula 5). La verificación de acceso se realiza como en un contexto no relacionado conT
yU
Solo se considera la validez del contexto inmediato de la expresión de asignación. [ Nota: la compilación de la expresión puede provocar efectos secundarios, como la creación de instancias de especializaciones de plantilla de clase y especializaciones de plantilla de función, la generación de funciones definidas de forma implícita, etc. Dichos efectos secundarios no están en el "contexto inmediato" y pueden hacer que el programa no se forme correctamente. "Nota final "
Esencialmente, esto implica una verificación de compatibilidad de acceso / existencia + tipo de argumento del operator=
, y nada más.
Sin embargo, Visual C ++ 11.0 no parece hacer la comprobación de acceso, mientras que g ++ 4.7.1 se ahoga en ello:
#include <iostream>
#include <type_traits>
#include <tuple>
using namespace std;
struct A {};
struct B { private: B& operator=( B const& ); };
template< class Type >
bool isAssignable() { return is_assignable< Type, Type >::value; }
int main()
{
wcout << boolalpha;
wcout << isAssignable< A >() << endl; // OK.
wcout << isAssignable< B >() << endl; // Uh oh.
}
Construyendo con Visual C ++ 11.0:
[D:/dev/test/so/assignable] > cl assignable.cpp assignable.cpp [D:/dev/test/so/assignable] > _
Construyendo con g ++ 4.7.1:
[D:/dev/test/so/assignable] > g++ assignable.cpp d:/bin/mingw/bin/../lib/gcc/i686-pc-mingw32/4.7.1/../../../../include/c++/4.7.1/type_traits: In substitution of ''template static decltype (((declval)()=(declval)(), std::__sfinae_types::__one()))std::__is_assignable_helper::__test(int) [with _ Tp1 = _Tp1; _Up1 = _Up1; _Tp = B; _Up = B] [with _Tp1 = B; _Up1 = B]'': d:/bin/mingw/bin/../lib/gcc/i686-pc-mingw32/4.7.1/../../../../include/c++/4.7.1/type_traits:1055:68: required from ''constexpr const bool std::__is_assignable_helper::value'' d:/bin/mingw/bin/../lib/gcc/i686-pc-mingw32/4.7.1/../../../../include/c++/4.7.1/type_traits:1060:12: required from ''struct std::is_assignable'' assignable.cpp:10:59: required from ''bool isAssignable() [with Type = B]'' assignable.cpp:16:32: required from here assignable.cpp:7:24: error: ''B& B::operator=(const B&)'' is private In file included from d:/bin/mingw/bin/../lib/gcc/i686-pc-mingw32/4.7.1/../../../../include/c++/4.7.1/bits/move.h:57:0, from d:/bin/mingw/bin/../lib/gcc/i686-pc-mingw32/4.7.1/../../../../include/c++/4.7.1/bits/stl_pair.h:61, from d:/bin/mingw/bin/../lib/gcc/i686-pc-mingw32/4.7.1/../../../../include/c++/4.7.1/bits/stl_algobase.h:65, from d:/bin/mingw/bin/../lib/gcc/i686-pc-mingw32/4.7.1/../../../../include/c++/4.7.1/bits/char_traits.h:41, from d:/bin/mingw/bin/../lib/gcc/i686-pc-mingw32/4.7.1/../../../../include/c++/4.7.1/ios:41, from d:/bin/mingw/bin/../lib/gcc/i686-pc-mingw32/4.7.1/../../../../include/c++/4.7.1/ostream:40, from d:/bin/mingw/bin/../lib/gcc/i686-pc-mingw32/4.7.1/../../../../include/c++/4.7.1/iostream:40, from assignable.cpp:1: d:/bin/mingw/bin/../lib/gcc/i686-pc-mingw32/4.7.1/../../../../include/c++/4.7.1/type_traits:1049:2: error: within this context d:/bin/mingw/bin/../lib/gcc/i686-pc-mingw32/4.7.1/../../../../include/c++/4.7.1/type_traits: In substitution of ''template static decltype (((declval)()=(declval)(), std::__sfinae_types::__one())) std::__is_assignable_helper::__test(int) [with _ Tp1 = _Tp1; _Up1 = _Up1; _Tp = B; _Up = B] [with _Tp1 = B; _Up1 = B]'': d:/bin/mingw/bin/../lib/gcc/i686-pc-mingw32/4.7.1/../../../../include/c++/4.7.1/type_traits:1055:68: required from ''constexpr const bool std::__is_assignable_helper::value'' d:/bin/mingw/bin/../lib/gcc/i686-pc-mingw32/4.7.1/../../../../include/c++/4.7.1/type_traits:1060:12: required from ''struct std::is_assignable'' assignable.cpp:10:59: required from ''bool isAssignable() [with Type = B]'' assignable.cpp:16:32: required from here assignable.cpp:7:24: error: ''B& B::operator=(const B&)'' is private In file included from d:/bin/mingw/bin/../lib/gcc/i686-pc-mingw32/4.7.1/../../../../include/c++/4.7.1/bits/move.h:57:0, from d:/bin/mingw/bin/../lib/gcc/i686-pc-mingw32/4.7.1/../../../../include/c++/4.7.1/bits/stl_pair.h:61, from d:/bin/mingw/bin/../lib/gcc/i686-pc-mingw32/4.7.1/../../../../include/c++/4.7.1/bits/stl_algobase.h:65, from d:/bin/mingw/bin/../lib/gcc/i686-pc-mingw32/4.7.1/../../../../include/c++/4.7.1/bits/char_traits.h:41, from d:/bin/mingw/bin/../lib/gcc/i686-pc-mingw32/4.7.1/../../../../include/c++/4.7.1/ios:41, from d:/bin/mingw/bin/../lib/gcc/i686-pc-mingw32/4.7.1/../../../../include/c++/4.7.1/ostream:40, from d:/bin/mingw/bin/../lib/gcc/i686-pc-mingw32/4.7.1/../../../../include/c++/4.7.1/iostream:40, from assignable.cpp:1: d:/bin/mingw/bin/../lib/gcc/i686-pc-mingw32/4.7.1/../../../../include/c++/4.7.1/type_traits:1049:2: error: within this context d:/bin/mingw/bin/../lib/gcc/i686-pc-mingw32/4.7.1/../../../../include/c++/4.7.1/type_traits: In instantiation of ''constexpr const bool std::__is_assignable_helper::value'': d:/bin/mingw/bin/../lib/gcc/i686-pc-mingw32/4.7.1/../../../../include/c++/4.7.1/type_traits:1060:12: required from ''struct std::is_assignable'' assignable.cpp:10:59: required from ''bool isAssignable() [with Type = B]'' assignable.cpp:16:32: required from here assignable.cpp:7:24: error: ''B& B::operator=(const B&)'' is private In file included from d:/bin/mingw/bin/../lib/gcc/i686-pc-mingw32/4.7.1/../../../../include/c++/4.7.1/bits/move.h:57:0, from d:/bin/mingw/bin/../lib/gcc/i686-pc-mingw32/4.7.1/../../../../include/c++/4.7.1/bits/stl_pair.h:61, from d:/bin/mingw/bin/../lib/gcc/i686-pc-mingw32/4.7.1/../../../../include/c++/4.7.1/bits/stl_algobase.h:65, from d:/bin/mingw/bin/../lib/gcc/i686-pc-mingw32/4.7.1/../../../../include/c++/4.7.1/bits/char_traits.h:41, from d:/bin/mingw/bin/../lib/gcc/i686-pc-mingw32/4.7.1/../../../../include/c++/4.7.1/ios:41, from d:/bin/mingw/bin/../lib/gcc/i686-pc-mingw32/4.7.1/../../../../include/c++/4.7.1/ostream:40, from d:/bin/mingw/bin/../lib/gcc/i686-pc-mingw32/4.7.1/../../../../include/c++/4.7.1/iostream:40, from assignable.cpp:1: d:/bin/mingw/bin/../lib/gcc/i686-pc-mingw32/4.7.1/../../../../include/c++/4.7.1/type_traits:1055:68: error: within this context [D:/dev/test/so/assignable] > _
Entonces, resumiendo, el estándar std::is_assignable
parece ser de utilidad muy limitada, y en el momento de esta escritura no se puede confiar en el código portátil.
EDITAR: Se reemplazó <utility>
con <type_traits
correctos. Curiosamente no importaba para g ++. Ni siquiera por el mensaje de error, así que solo dejo que sea como era.
C ++ 11 eliminó el requisito de que el tipo de valor de todos los contenedores sea CopyConstructible and Assignable (aunque las operaciones específicas en contenedores pueden imponer estos requisitos). En teoría, eso debería permitir definir, por ejemplo, std::deque<const Foo>
, que no era posible en C ++ 03.
Inesperadamente, gcc 4.7.2 produjo su vómito habitual de errores incomprensibles [1] cuando intenté esto, pero al menos el error hizo legible el error y el sonido con libc ++ lo compiló sin errores.
Ahora, cuando dos compiladores diferentes producen resultados diferentes, siempre me pregunto cuál es la respuesta correcta, por lo que busqué todas las referencias que pude encontrar para const / asignar / tipos de valor / contenedores, etc., etc., encontré casi una década de preguntas y respuestas muy similares, algunas de ellas aquí en SO y otras en las distintas listas de correo de C ++, entre otros lugares, incluido el buganizador Gnu, que se pueden resumir básicamente en el siguiente diálogo.
P: ¿Por qué no puedo declarar std::vector<const int>
(como un ejemplo simplificado)
A: ¿Por qué demonios quieres hacer eso? Es absurdo.
P: Bueno, tiene sentido para mí. ¿Por qué no puedo hacerlo?
R: Porque el estándar requiere que los tipos de valor sean asignables.
P: Pero no estoy planeando asignarlos. Quiero que sean constantes después de haberlos creado.
A: No es así como funciona. ¡Próxima pregunta!
con un ligero trazo de:
A2: C ++ 11 ha decidido permitir eso. Tendrás que esperar. Mientras tanto, reconsidere su diseño ridículo.
Estas no parecen respuestas muy convincentes, aunque posiblemente estoy sesgada porque caigo en la categoría de "pero tiene sentido para mí". En mi caso, me gustaría tener un contenedor con forma de pila en el que las cosas que se empujan en la pila son inmutables hasta que se abran, lo que no me parece una cosa particularmente extraña de querer poder expresar con un tipo sistema.
De todos modos, comencé a pensar en la respuesta, "el estándar requiere que todos los tipos de valor de los contenedores sean asignables". Y, por lo que puedo ver, ahora que encontré una copia antigua de un borrador del estándar C ++ 03, eso es cierto; lo hizo.
Por otro lado, el tipo de valor de std::map
es std::pair< const Key, T>
que no me parece que sea asignable. Sin embargo, lo intenté de nuevo con std::deque<std::tuple<const Foo>>
, y gcc procedió a compilarlo sin pestañear. Así que al menos tengo algún tipo de solución.
Luego intenté imprimir el valor de std::is_assignable<const Foo, const Foo>
y std::is_assignable<std::tuple<const Foo>, const std::tuple<const Foo>>
, y resulta que el primero es reportado como no asignable, como es de esperar, pero el último es reportado como asignable (tanto para clang como para gcc). Por supuesto, no es realmente asignable; intentando compilar a = b;
es rechazado por gcc con el error: assignment of read-only location
reclamación error: assignment of read-only location
(esto fue casi el único mensaje de error que encontré en esta búsqueda que fue realmente fácil de entender). Sin embargo, sin el intento de hacer una asignación, tanto clang como gcc están igualmente felices de crear una instancia del deque<const>
, y el código parece funcionar bien.
Ahora, si std::tuple<const int>
realmente es asignable, entonces no puedo quejarme de la inconsistencia en el estándar C++03
y, a decir verdad, a quién le importa, pero me parece inquietante que dos estándares diferentes las implementaciones de la biblioteca informan que un tipo es asignable cuando, de hecho, el intento de asignar a una referencia de él llevará a un error de compilación (muy sensible). Es posible que en algún momento quiera usar la prueba en una plantilla SFINAE, y según lo que vi hoy, no parece muy confiable.
Entonces, ¿hay alguien que pueda arrojar algo de luz sobre la pregunta (en el título): qué significa realmente Assignable? Y dos preguntas extra:
1) ¿El comité realmente quiso permitir la creación de instancias de contenedores con tipos de valores const
, o tenían algún otro caso no asignable en mente?
2) ¿Existe realmente una diferencia significativa entre las constnesses de const Foo
y std::tuple<const Foo>
?
[1] Para los realmente curiosos, aquí está el mensaje de error producido por gcc al intentar compilar la declaración de std::deque<const std::string>
(con algunos finales de línea agregados, y una explicación si se desplaza hacia abajo) suficiente):
In file included from /usr/include/x86_64-linux-gnu/c++/4.7/./bits/c++allocator.h:34:0,
from /usr/include/c++/4.7/bits/allocator.h:48,
from /usr/include/c++/4.7/string:43,
from /usr/include/c++/4.7/random:41,
from /usr/include/c++/4.7/bits/stl_algo.h:67,
from /usr/include/c++/4.7/algorithm:63,
from const_stack.cc:1:
/usr/include/c++/4.7/ext/new_allocator.h: In instantiation of ‘class __gnu_cxx::new_allocator<const std::basic_string<char> >’:
/usr/include/c++/4.7/bits/allocator.h:89:11: required from ‘class std::allocator<const std::basic_string<char> >’
/usr/include/c++/4.7/bits/stl_deque.h:489:61: required from ‘class std::_Deque_base<const std::basic_string<char>, std::allocator<const std::basic_string<char> > >’
/usr/include/c++/4.7/bits/stl_deque.h:728:11: required from ‘class std::deque<const std::basic_string<char> >’
const_stack.cc:112:27: required from here
/usr/include/c++/4.7/ext/new_allocator.h:83:7:
error: ‘const _Tp* __gnu_cxx::new_allocator< <template-parameter-1-1> >::address(
__gnu_cxx::new_allocator< <template-parameter-1-1> >::const_reference) const [
with _Tp = const std::basic_string<char>;
__gnu_cxx::new_allocator< <template-parameter-1-1> >::const_pointer =
const std::basic_string<char>*;
__gnu_cxx::new_allocator< <template-parameter-1-1> >::const_reference =
const std::basic_string<char>&]’ cannot be overloaded
/usr/include/c++/4.7/ext/new_allocator.h:79:7:
error: with ‘_Tp* __gnu_cxx::new_allocator< <template-parameter-1-1> >::address(
__gnu_cxx::new_allocator< <template-parameter-1-1> >::reference) const [
with _Tp = const std::basic_string<char>;
__gnu_cxx::new_allocator< <template-parameter-1-1> >::pointer = const std::basic_string<char>*;
__gnu_cxx::new_allocator< <template-parameter-1-1> >::reference = const std::basic_string<char>&]’
Entonces, lo que está sucediendo aquí es que el estándar (§ 20.6.9.1) insiste en que el asignador predeterminado tiene funciones miembro:
pointer address(reference x) const noexcept;
const_pointer address(const_reference x) const noexcept;
pero si lo instancia con un argumento de plantilla const
(que aparentemente es UB), entonces reference
y const_reference
son del mismo tipo, y por lo tanto las declaraciones se duplican. (El cuerpo de la definición es idéntico, por lo que vale). En consecuencia, ningún contenedor que tenga en cuenta el asignador puede tratar con un tipo de valor explícitamente const
. Ocultar la const
dentro de una tuple
permite al asignador crear una instancia. Este requisito del asignador del estándar se usó para justificar el cierre de al menos un par de errores antiguos de libstdc ++ sobre problemas con std::vector<const int>
, aunque no me parece un punto de principio sólido. También libc ++ resuelve el problema de la manera más obvia y simple, que es proporcionar una especialización del allocator<const T>
con las declaraciones de funciones duplicadas eliminadas.
Le doy esto a pero quería agregar un par de notas para futuras referencias.
Como dice Alf, std::is_*_assignable
realmente solo verifica la existencia (explícita o implícita) de un operador de asignación apropiado. No necesariamente verifican si estará bien formado si se crea una instancia. Esto funciona bien para los operadores de asignación predeterminados. Si hay un miembro declarado const
, los operadores de asignación predeterminados se eliminarán. Si una clase base tiene un operador de asignación eliminado, se eliminará el operador de asignación predeterminado. Así que si dejas que los valores predeterminados hagan lo suyo, debería estar bien.
Sin embargo, si declara operator=
, se convierte en su responsabilidad (si le importa) asegurarse de que se elimine correctamente. Por ejemplo, esto se compilará y ejecutará (al menos con clang), e informa que C
asignable:
#include <iostream>
#include <type_traits>
#include <tuple>
using namespace std;
struct A { const int x; A() : x() {}};
struct C {
struct A a;
C& operator=( C const& other);
};
template< class Type >
bool isAssignable() { return is_assignable< Type&, const Type& >::value; }
int main()
{
wcout << boolalpha;
wcout << isAssignable< A >() << endl; // false
wcout << isAssignable< C >() << endl; // true
C c1;
C c2;
}
La ausencia de la definición del operador de asignación no se nota hasta el tiempo de enlace, y en este caso no se hace en absoluto porque el operador de asignación nunca se utiliza. Pero tenga en cuenta que un uso de C::operator=
contingent en std::is_assignable
se permitiría compilar. Por supuesto, no podría haber definido C::operator=
de una manera que resulte en asignar a su miembro a
, porque ese miembro no es asignable.
Pero ese no es un ejemplo particularmente interesante. Lo que es interesante es el uso de plantillas, como el problema std::tuple
que inició toda esta pregunta. Agreguemos un par de plantillas a lo anterior, y definamos C::operator=
través de la asignación a su miembro a
:
using namespace std;
template<bool> struct A {
A() : x() {}
const int x;
};
template<bool B> struct C {
struct A<B> a;
C& operator=( C const& other) {
this->a = other.a;
return *this;
}
};
template< class Type >
bool isAssignable() { return is_assignable< Type&, const Type& >::value; }
int main()
{
wcout << boolalpha;
wcout << isAssignable< A<false> >() << endl; // false
wcout << isAssignable< C<false> >() << endl; // true
C<false> c1;
C<false> c2;
c1 = c2; // Bang
return 0;
}
Sin la asignación al final, el código se compila y ejecuta (debajo del lenguaje lógico 3.3) e informa que A<false>
no es asignable (correcto) pero que C<false>
es asignable (¡sorpresa!). El intento real de usar C::operator=
revela la verdad, porque no es hasta ese momento que el compilador intenta crear una instancia de ese operador. Hasta entonces, y a través de las instancias de is_assignable
, el operador era solo una declaración de una interfaz pública, que es, como dice Alf, todo lo que std::is_assignable
realmente está buscando.
Uf.
Entonces, en conclusión, esto es una deficiencia en las implementaciones estándar y estándar de la biblioteca con respecto a los objetos agregados estándar cuyo operator=
debe eliminarse si alguno de los tipos de componentes no es asignable. Para std::tuple
, el § 20.4.2.2 enumera como requisitos para operator=
que todos los tipos de componentes sean asignables, y existen requisitos similares para otros tipos, pero no creo que ese requisito requiera que los implementadores de la biblioteca eliminen el operator=
aplicable operator=
.
Pero, por lo que puedo ver, nada impide que las implementaciones de la biblioteca realicen la eliminación (excepto el factor de molestia de la eliminación condicional de los operadores de asignación). En mi opinión, después de obsesionarme con esto durante un par de días, deberían hacerlo, y además la norma debería exigir que lo hagan. De lo contrario, el uso confiable de is_assignable
imposible.