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
. Esto por lo general va a ser
make_reference_v<value_type>
, pero la posibilidad de especializarlo permite cosas como que
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
, que convierte una referencia a T (
) en una referencia a rvalue de T (
). El problema es que
no funciona sobre el concepto de referencia, sino sobre el tipo
concretamente. Esto significa que algoritmos que permutan una secuencia y que por lo tanto dependen de
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
.
Por ejemplo, supongamos un iterador que contiene dos iteradores
e
, implementando una vista unificada de dos secuencias, y define su tipo de referencia como
pair<It1::reference, It2::reference>
. Supongamos que
es
, 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
devuelve
,
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
como tipo de referencia, y algunos de los que no, como
, 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
, 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
están mal y ya no serán posibles, o si lo son y hay que arreglar
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
.