Debería std::move ser especializable?
Publicado el 18 de agosto de 2020
El diseño de iteradores de la STL permite a un tipo de iterador especializar cuál va a ser el tipo de referencia que va a usar, definiendo dentro el tipo miembro
reference
. Esto por lo general va a ser
make_reference_v<value_type>
, pero la posibilidad de especializarlo permite cosas como que
vector<bool>
use un solo bit por bool, y aun así pueda tener iteradores. Para esto define un tipo de referencia que semánticamente actúa como representante de un bit, mientras que sintácticamente actúa como una referencia a bool. Para esto, la STL implícitamente define el concepto de referencia a T como algo que es convertible a T, a lo que se le puede asignar un T y que tiene constructor de copia. Otro tipo de referencia interesante es el par o la tupla de referencias, que permite implementar una vista sobre varios contenedores separados como una sola estructura.
C++11 introdujo las referencias a rvalues, que identifican valores que van a expirar pronto y de los que por lo tanto se puede mover. Esto permite optimizar tipos que son propietarios de recursos, como memoria, sustituyendo las caras operaciones de copia por baratas operaciones de mover en situaciones en las que aquello de lo que se está moviendo va a dejar de ser necesario. Por defecto, se mueve de todas las temporales o prvalues. Además de eso, C++11 introdujo en la biblioteca la función
std::move
, que convierte una referencia a T (
T &
) en una referencia a rvalue de T (
T &&
). El problema es que
std::move
no funciona sobre el concepto de referencia, sino sobre el tipo
T &
concretamente. Esto significa que algoritmos que permutan una secuencia y que por lo tanto dependen de
std::move
para poder hacerlo de la forma más eficiente posible, serán ineficientes si se usan con iteradores que usen un tipo de referencia distinto a
T &
.
Por ejemplo, supongamos un iterador que contiene dos iteradores
It1
e
It2
, implementando una vista unificada de dos secuencias, y define su tipo de referencia como
pair<It1::reference, It2::reference>
. Supongamos que
It2::reference
es
std::vector<int> &
, y por lo tanto nuestro tipo de referencia sería por ejemplo
pair<int &, std::vector<int> &>
. Un algoritmo que permutara las dos secuencias por separado movería correctamente los vectores resultando en 0 reservas de memoria. Sin embargo, el mismo algoritmo sobre las dos secuencias unidas haría copias innecesarias de vectores y sería por lo tanto mucho más lento, sin que ello suponga ningún beneficio. El problema está en que mientras que
std::move(T &)
devuelve
T &&
,
std::move(pair<int &, std::vector<int> &>)
no devuelve
pair<int &&, std::vector<int> &&>
, sino
pair<int &>, std::vector<int> &> &&
, lo cual no es correcto si consideramos que un par de referencias es una referencia.
Lo que parece es que el concepto de referencia se diseñó en C++98, cuando las referencias a rvalues no existían, y hace todo lo que una referencia tenía que hacer entonces. Cuando se introdujeron las referencias a rvalues el concepto no fue revisado, y eso ha hecho que haya un desequilibrio entre dos partes de la biblioteca estándar. Por suerte es un caso muy remoto ya que la mayor parte de contenedores usan
T &
como tipo de referencia, y algunos de los que no, como
vector<bool>
, trabajan sobre un tipo trivial así que no se ven afectados por esto. Pero eso no hace desaparecer el problema, sólo lo hace más raro. Lo cierto es que en C++ ahora mismo, al menos en lo que respecta a iteradores y algoritmos, las referencias son un concepto, y
std::move
, que es una operación muy importante sobre referencias y muy usada en algoritmos, no tiene en cuenta esto. Si las referencias dejan de ser un concepto y cosas como
vector<bool>
están mal y ya no serán posibles, o si lo son y hay que arreglar
std::move
es una conversación que el estándar debería tener para decidir en una dirección o en otra. Además de esto, sería necesario identificar qué otras operaciones sobre referencias son incorrectas bajo el nuevo concepto de referencia, y decidir si hay que arreglar estas también. Por lo pronto, se me ocurre
std::swap
.
Logo of RSS.