¿Por qué usar argumentos variádicos ahora cuando las listas de inicializadores están disponibles?


Me he estado preguntando cuáles son las ventajas de los argumentos variádicos sobre las listas de inicializadores. Ambos ofrecen la misma capacidad: pasar un número indefinido de argumentos a una función.

Lo que personalmente creo es que las listas de inicializadores son un poco más elegantes. La sintaxis es menos incómoda.

Además, parece que las listas inicializadoras tienen un rendimiento significativamente mejor a medida que aumenta el número de argumentos.

Entonces, ¿qué me falta, además de la posibilidad de usar use variadic argumentos en C también?

Author: Andy Prowl, 2013-03-18

2 answers

Si por argumentos variádicos te refieres a las elipses (como en void foo(...)), entonces esas se vuelven más o menos obsoletas por plantillas variádicas en lugar de por listas inicializadoras - todavía podría haber algunos casos de uso para las elipses cuando se trabaja con SFINAE para implementar (por ejemplo) rasgos de tipo, o para compatibilidad con C, pero hablaré de casos de uso ordinarios aquí.

Las plantillas variádicas, de hecho, permiten diferentes tipos para el paquete de argumentos (de hecho, cualquier tipo), mientras que los valores de una lista inicializadora deben ser convertibles al tipo subyacente de la lista initalizer (y no se permiten conversiones de estrechamiento):

#include <utility>

template<typename... Ts>
void foo(Ts...) { }

template<typename T>
void bar(std::initializer_list<T>) { }

int main()
{
    foo("Hello World!", 3.14, 42); // OK
    bar({"Hello World!", 3.14, 42}); // ERROR! Cannot deduce T
}

Debido a esto, las listas inicializadoras se usan con menos frecuencia cuando se requiere deducción de tipos, a menos que el tipo de los argumentos esté destinado a ser homogéneo. Las plantillas Variadic, por otro lado, proporcionan una versión segura de tipo de la lista de argumentos elipses variadic.

También, invocando una función que toma un inicializador list requiere encerrar los argumentos en un par de llaves, que no es el caso de una función que toma un paquete de argumentos variádicos.

Finalmente (bueno, hay otras diferencias, pero estas son las más relevantes para tu pregunta), los valores en las listas de inicializadores son objetos const. Según el párrafo 18.9 / 1 del estándar C++11:

Un objeto de tipo initializer_list<E> proporciona acceso a un array de objetos de tipo const E. [...] Copiar una lista inicializadora hace no copiar el elementos subyacentes. [...]

Esto significa que aunque los tipos no copiables se pueden mover a una lista inicializadora, no se pueden mover fuera de ella. Esta limitación puede o no cumplir con los requisitos de un programa, pero generalmente hace que initializer lists sea una opción limitante para contener tipos no copiables.

De manera más general, de todos modos, cuando se utiliza un objeto como elemento de una lista inicializadora, haremos una copia de él (si es un lvalue) o nos alejaremos de él (si es un rvalue):

#include <utility>
#include <iostream>

struct X
{
    X() { }
    X(X const &x) { std::cout << "X(const&)" << std::endl; }
    X(X&&) { std::cout << "X(X&&)" << std::endl; }
};

void foo(std::initializer_list<X> const& l) { }

int main()
{
    X x, y, z, w;
    foo({x, y, z, std::move(w)}); // Will print "X(X const&)" three times
                                  // and "X(X&&)" once
}

En otras palabras, las listas inicializadoras no se pueden usar para pasar argumentos por referencia (*), y mucho menos para realizar un reenvío perfecto:

template<typename... Ts>
void bar(Ts&&... args)
{
    std::cout << "bar(Ts&&...)" << std::endl;
    // Possibly do perfect forwarding here and pass the
    // arguments to another function...
}

int main()
{
    X x, y, z, w;
    bar(x, y, z, std::move(w)); // Will only print "bar(Ts&&...)"
}

(*) Debe tenerse en cuenta, sin embargo, que las listas inicializadoras (a diferencia de todos los demás contenedores de la Biblioteca Estándar de C++) tienen semántica de referencia, por lo tanto, aunque se realiza una copia/movimiento de los elementos al insertar elementos en una lista inicializadora, copiar la lista inicializadora en sí no causará cualquier copia / movimiento de los objetos contenidos (como se menciona en el párrafo de la Norma citada anteriormente):

int main()
{
    X x, y, z, w;
    auto l1 = {x, y, z, std::move(w)}; // Will print "X(X const&)" three times
                                       // and "X(X&&)" once

    auto l2 = l1; // Will print nothing
}
 47
Author: Andy Prowl,
Warning: date(): Invalid date.timezone value 'Europe/Kyiv', we selected the timezone 'UTC' for now. in /var/www/agent_stack/data/www/ajaxhispano.com/template/agent.layouts/content.php on line 61
2017-05-23 10:29:47

Brevemente, las funciones variádicas de estilo C producen menos código cuando se compilan que las plantillas variádicas de estilo C++, por lo que si le preocupa el tamaño binario o la presión de la caché de instrucciones, debería considerar implementar su funcionalidad con varargs en lugar de como una plantilla.

Sin embargo, las plantillas variadic son significativamente más seguras y producen mensajes de error mucho más utilizables, por lo que a menudo querrá envolver su función variadic fuera de línea con una plantilla variadic en línea y hacer que los usuarios llamen plantilla.

 1
Author: Jeffrey Yasskin,
Warning: date(): Invalid date.timezone value 'Europe/Kyiv', we selected the timezone 'UTC' for now. in /var/www/agent_stack/data/www/ajaxhispano.com/template/agent.layouts/content.php on line 61
2013-03-19 18:01:58