tipos - ¿Paquete de parámetros de C++, restringido para tener instancias de un solo tipo?
sintaxis de c++ (7)
Supongamos que realmente solo tiene sentido llamar a mi función en el caso de que cada argumento tenga el mismo tipo, aunque cualquier número de argumentos estaría bien.
En este caso, ¿por qué no usar std::initializer_list
?
template <typename T>
void func(std::initializer_list<T> li) {
for (auto ele : li) {
// process ele
cout << ele << endl;
}
}
Como @Yakk mencionó en el comentario, es posible que desee evitar las copias const
. En ese caso, puede copiar los punteros a std::initializer_list
:
// Only accept pointer type
template <typename T>
void func(std::initializer_list<T> li) {
for (auto ele : li) {
// process pointers, so dereference first
cout << *ele << endl;
}
}
O especializar la func
para los punteros:
// Specialize for pointer
template <typename T>
void func(std::initializer_list<T*> li) {
for (auto ele : li) {
// process pointers, so dereference first
cout << *ele << endl;
}
}
my_struct a, b, c;
func({a, b, c}); // copies
func({&a, &b, &c}); // no copies, and you can change a, b, c in func
Desde C ++ 11 podemos hacer funciones de plantilla que pueden aceptar cualquier secuencia de argumentos:
template <typename... Ts>
void func(Ts &&... ts) {
step_one(std::forward<Ts>(ts)...);
step_two(std::forward<Ts>(ts)...);
}
Sin embargo, supongamos que realmente solo tiene sentido llamar a mi función en el caso de que cada argumento tenga el mismo tipo, aunque cualquier número de argumentos estaría bien.
¿Cuál es la mejor manera de hacerlo, es decir, hay una buena manera de restringir las plantillas para crear un buen mensaje de error en ese caso, o idealmente, eliminar la func
de participar en la resolución de sobrecarga cuando los argumentos no coinciden?
Puedo hacerlo realmente concreto si ayuda:
Supongamos que tengo alguna estructura:
struct my_struct {
int foo;
double bar;
std::string baz;
};
Ahora, quiero poder hacer cosas como, imprimir a los miembros de la estructura para propósitos de depuración, serializar y deserializar la estructura, visitar a los miembros de la estructura en secuencia, etc. Tengo algo de código para ayudar con eso:
template <typename V>
void apply_visitor(V && v, my_struct & s) {
std::forward<V>(v)("foo", s.foo);
std::forward<V>(v)("bar", s.bar);
std::forward<V>(v)("baz", s.baz);
}
template <typename V>
void apply_visitor(V && v, const my_struct & s) {
std::forward<V>(v)("foo", s.foo);
std::forward<V>(v)("bar", s.bar);
std::forward<V>(v)("baz", s.baz);
}
template <typename V>
void apply_visitor(V && v, my_struct && s) {
std::forward<V>(v)("foo", std::move(s).foo);
std::forward<V>(v)("bar", std::move(s).bar);
std::forward<V>(v)("baz", std::move(s).baz);
}
(Parece un poco laborioso generar código como este, pero hace un tiempo hice una pequeña biblioteca para ayudar con eso).
Entonces, ahora me gustaría extenderlo para que pueda visitar dos instancias de my_struct
al mismo tiempo. El uso de eso es, ¿qué pasa si quiero implementar operaciones de igualdad o comparación? En la documentación de boost::variant
, la denominan "visitación binaria" en contraste con la "visitación unaria".
Probablemente, nadie querrá hacer más que una visita binaria. Pero supongamos que quiero hacer una visita general. Entonces, parece que esto, supongo
template <typename V, typename ... Ss>
void apply_visitor(V && v, Ss && ... ss) {
std::forward<V>(v)("foo", (std::forward<Ss>(ss).foo)...);
std::forward<V>(v)("bar", (std::forward<Ss>(ss).bar)...);
std::forward<V>(v)("baz", (std::forward<Ss>(ss).baz)...);
}
Pero ahora, se está volviendo un poco más arduo: si alguien pasa una serie de tipos que no son ni siquiera el mismo tipo de estructura, el código aún puede compilarse y hacer algo totalmente inesperado para el usuario.
Pensé en hacerlo así:
template <typename V, typename ... Ss>
void apply_visitor(V && v, Ss && ... ss) {
auto foo_ptr = &my_struct::foo;
std::forward<V>(v)("foo", (std::forward<Ss>(ss).*foo_ptr)...);
auto bar_ptr = &my_struct::bar;
std::forward<V>(v)("bar", (std::forward<Ss>(ss).*bar_ptr)...);
auto baz_ptr = &my_struct::baz;
std::forward<V>(v)("baz", (std::forward<Ss>(ss).*baz_ptr)...);
}
Eso al menos causará un error de compilación si lo usan con tipos que no coinciden. Pero, también está ocurriendo demasiado tarde: sucede después de que se resuelven los tipos de plantilla, y después de la resolución de sobrecarga, supongo
Pensé en usar SFINAE, como, en lugar de devolver void, usar std::enable_if_t
y verificar alguna expresión std::is_same<std::remove_cv_t<std::remove_reference_t<...>>
para cada tipo en el paquete de parámetros.
Pero para uno, esa expresión de SFINAE es bastante complicada, y para dos, también tiene un inconveniente: supongamos que alguien tiene una struct my_other_struct : my_struct { ... }
clase derivada struct my_other_struct : my_struct { ... }
, y quieren usarla con el mecanismo de visitantes, por lo que algunos de los parámetros son my_struct
y otros son my_other_struct
. Idealmente, el sistema convertiría todas las referencias a my_struct
y aplicaría al visitante de esa manera, y siguiendo el ejemplo que dí anteriormente con los punteros de miembro foo_ptr
, bar_ptr
, baz_ptr
haría lo correcto allí, pero no me queda claro cómo escribir Una restricción como la de SFINAE: ¿Tendría que intentar encontrar una base común de todos los parámetros que supongo?
¿Hay una buena manera de conciliar esas preocupaciones en general?
Con std::common_type
, esto es sencillo:
template <class... Args, class = std::common_type_t<Args...>>
void foo(Args &&... args) {
}
Sin embargo, solo se garantizará que sea SFINAE
SFINAE desde C++17
adelante. Clang
y GCC
ya lo implementan de esa manera.
Creo que puedes hacer una función como esta y revisar los argumentos dentro de tu función.
template <typename T, typename... Args> bool check_args(T a, Args args)
{
static string type;
if(type == "") type = typeid(a).name;
else if(type != typeid(a).name) return false;
else return check_args(args...);
}
bool check_args() {return true;}
Este es un rasgo de tipo que puede usar en static_assert
o std::enable_if
a su std::enable_if
.
template <class T, class ... Ts>
struct are_all_same : conjunction<std::is_same<T, Ts>...>{};
template <class Ts...>
struct conjunction : std::true_type{};
template <class T, class ... Ts>
struct conjunction<T, Ts...> :
std::conditional<T::value, conjunction<Ts...>, std::false_type>::type {};
Simplemente comprueba cada tipo con el primero y falla si alguno es diferente.
Creo que usar std::common_type
sería algo como esto:
template <class ... Args>
typename std::common_type<Args...>::type common_type_check(Args...);
void common_type_check(...);
template <class ... Ts>
struct has_common_type :
std::integral_constant<
bool,
!std::is_same<decltype(common_type_check(std::declval<Ts>()...)), void>::value> {};
Luego puedes hacer static_assert(std::has_common_type<Derived, Base>::value, "")
Por supuesto, este método no es infalible, ya que common_type
tiene algunas restricciones cuando se trata de clases base:
struct A {};
struct B : A{};
struct C : A{};
struct D : C{};
struct E : B{};
static_assert(has_common_type<E, D, C, A, B>::value, ""); //Fails
static_assert(has_common_type<A, B, C, D, E>::value, ""); //Passes
Esto se debe a que la plantilla primero intenta obtener el tipo común entre D
y E
(es decir, auto a = bool() ? D{}: E{};
no se compila).
Esto toma un tipo de entrada arbitrario y mueve su referencia r / lvalue a un tipo de Out
en una conversión implícita.
template<class Out>
struct forward_as {
template<class In,
std::enable_if_t<std::is_convertible<In&&,Out>{}&&!std::is_base_of<Out,In>{},int>* =nullptr
>
Out operator()(In&& in)const{ return std::forward<In>(in); }
Out&& operator()(Out&& in)const{ return std::forward<Out>(in); }
template<class In,
std::enable_if_t<std::is_convertible<In&,Out&>{},int>* =nullptr
>
Out& operator()(In& in)const{ return in; }
template<class In,
std::enable_if_t<std::is_convertible<In const&,Out const&>{},int>* =nullptr
>
Out const& operator()(In const& in)const{ return in; }
};
Con esto, aquí está nuestro n_ ary apply_visitor
:
template <typename V, typename ... Ss,
decltype(std::void_t<
std::result_of_t<forward_as<my_struct>(Ss)>...
>(),int())* =nullptr
>
void apply_visitor(V && v, Ss && ... ss) {
auto convert = forward_as<my_struct>{};
std::forward<V>(v)("foo", (convert(std::forward<Ss>(ss)).foo)...);
std::forward<V>(v)("bar", (convert(std::forward<Ss>(ss)).bar)...);
std::forward<V>(v)("baz", (convert(std::forward<Ss>(ss)).baz)...);
}
que no coincide si forward_as<my_struct>
no puede evaluar en Ss
.
Lo que realmente quieres es algo como:
template<typename T, T ... args>
void myFunc(T ... args);
Pero claramente lo anterior no es sintaxis legal. Puede solucionar este problema, sin embargo, con un using
plantillas para ayudarle. Así que la idea es esta:
template<typename T, size_t val>
using IdxType = T;
Lo anterior no tiene un propósito real: un IdxType<T, n>
es solo una T
para cualquier n
. Sin embargo, te permite hacer esto:
template<typename T, size_t ... Indices>
void myFunc(IdxType<T, Indices> ... args);
Lo que es genial, ya que es precisamente lo que necesita para obtener un conjunto variado de parámetros de tipo idéntico. El único problema que queda es que no puedes hacer cosas como myFunc(obj1, obj2, obj3)
, ya que el compilador no podrá deducir los Indices
necesarios; tendrás que hacer myFunc<1,2,3>(obj1, obj2, obj3)
, que es feo. Afortunadamente, puedes make_index_sequence
de esto envolviendo una función auxiliar que se encarga de la generación del índice por ti usando make_index_sequence
.
A continuación se muestra un ejemplo completo, que es algo similar a su visitante (demostración en vivo here ):
template<typename T, size_t sz>
using IdxType = T;
struct MyType
{};
struct Visitor
{
void operator() (const MyType&)
{
std::cout << "Visited" << std::endl;
}
};
template <typename V>
void apply_visitor(std::index_sequence<>, V && v)
{
}
template <typename V, typename T, size_t FirstIndex, size_t ... Indices>
void apply_visitor(std::index_sequence<FirstIndex, Indices...>, V && v, T && first, IdxType<T, Indices> && ... ss) {
std::forward<V>(v)(std::forward<T>(first));
apply_visitor(std::index_sequence<Indices...>(), std::forward<V>(v), std::forward<T>(ss) ...);
}
template <typename V, typename T, typename ... Rest>
void do_apply_visitor(V && v, T && t, Rest && ... rest )
{
apply_visitor(std::make_index_sequence<sizeof...(Rest)+1>(), v, t, rest ... );
}
int main()
{
Visitor v;
do_apply_visitor(v, MyType{}, MyType{}, MyType{});
return 0;
}
Una posible solución sería utilizar una función de tiempo de compilación como are_same
en el siguiente ejemplo:
#include <type_traits>
template<typename T, typename... O>
constexpr bool are_same() {
bool b = true;
int arr[] = { (b = b && std::is_same<T, O>::value, 0)... };
return b;
}
int main() {
static_assert(are_same<int, int, int>(), "!");
static_assert(not are_same<int, double, int>(), "!");
}
Úsalo como sigue:
template <typename... Ts>
void func(Ts &&... ts) {
static_assert(are_same<Ts...>(), "!");
step_one(std::forward<Ts>(ts)...);
step_two(std::forward<Ts>(ts)...);
}
Tendrá un buen mensaje de error de compilación según lo solicitado.