massimiliano
Acabo de ver a Stephan T. Lavavej hablar en CppCon 2018
en “Deducción de argumentos de plantilla de clase”, donde en algún punto de paso dice:
En C ++, la información de tipo casi nunca fluye hacia atrás … Tuve que decir “casi” porque hay uno o dos casos, posiblemente más pero muy pocos.
A pesar de tratar de averiguar a qué casos se podría estar refiriendo, no pude encontrar nada. De ahí la pregunta:
¿En qué casos el estándar C ++ 17 exige que la información de tipo se propague hacia atrás?
Yakk – Adam Nevraumont
Aquí hay al menos un caso:
struct foo {
template<class T>
operator T() const {
std::cout << sizeof(T) << "\n";
return {};
}
};
si lo haces foo f; int x = f; double y = f;
la información de tipo fluirá “hacia atrás” para descubrir qué T
es en operator T
.
Puedes usar esto de una manera más avanzada:
template<class T>
struct tag_t {using type=T;};
template<class F>
struct deduce_return_t {
F f;
template<class T>
operator T()&&{ return std::forward<F>(f)(tag_t<T>{}); }
};
template<class F>
deduce_return_t(F&&)->deduce_return_t<F>;
template<class...Args>
auto construct_from( Args&&... args ) {
return deduce_return_t{ [&](auto ret){
using R=typename decltype(ret)::type;
return R{ std::forward<Args>(args)... };
}};
}
entonces ahora puedo hacer
std::vector<int> v = construct_from( 1, 2, 3 );
y funciona.
Por supuesto, ¿por qué no simplemente hacer {1,2,3}
? Bien, {1,2,3}
no es una expresión.
std::vector<std::vector<int>> v;
v.emplace_back( construct_from(1,2,3) );
que, sin duda, requiere un poco más de magia: ejemplo en vivo. (Tengo que hacer que el retorno deducido haga una verificación de F de SFINAE, luego haga que la F sea compatible con SFINAE, y Tengo que bloquear std::initializer_list en el operador deduce_return_t T.)
-
Respuesta muy interesante, y aprendí un nuevo truco, ¡así que muchas gracias! Tuve que agregar una pauta de deducción de plantilla para haz que tu ejemplo compilepero aparte de eso, ¡funciona a las mil maravillas!
– Massimiliano
12 de noviembre de 2018 a las 22:28
-
los
&&
calificador en eloperator T()
es un gran toque; ayuda a evitar la mala interacción conauto
causando un error de compilación siauto
se usa mal aquí.– Justin
12 de noviembre de 2018 a las 22:42
-
Eso es muy impresionante, ¿podría señalarme alguna referencia / hablar sobre la idea en el ejemplo? o tal vez es original 🙂 …
– llllllllll
12 de noviembre de 2018 a las 23:59
-
@lili ¿Qué idea? Cuento 5: ¿Usar el operador T para deducir tipos de devolución? ¿Usando etiquetas para pasar el tipo deducido a una lambda? ¿Utiliza operadores de conversión para crear objetos de ubicación personalizados? conectando los 4?
– Yakk – Adam Nevraumont
13 de noviembre de 2018 a las 0:13
-
@lili El ejemplo de “forma más avanzada” es, como dije, solo 4 o más ideas pegadas. Hice el pegado sobre la marcha para esta publicación, pero ciertamente he visto muchos pares o incluso trillizos de esos usados juntos. Es un montón de técnicas bastante oscuras (como se queja Tootsie), pero nada novedoso.
– Yakk – Adam Nevraumont
13 de noviembre de 2018 a las 2:26
Shafik Yaghmour
Stephan T. Lavavej explicó el caso del que hablaba en un tuit:
El caso en el que estaba pensando es donde puede tomar la dirección de una función sobrecargada/plantillada y si se usa para inicializar una variable de un tipo específico, eso eliminará la ambigüedad de cuál desea. (Hay una lista de lo que elimina la ambigüedad).
podemos ver ejemplos de esto de página cppreference en la dirección de la función sobrecargadahe exceptuado algunos a continuación:
int f(int) { return 1; }
int f(double) { return 2; }
void g( int(&f1)(int), int(*f2)(double) ) {}
int main(){
g(f, f); // selects int f(int) for the 1st argument
// and int f(double) for the second
auto foo = []() -> int (*)(int) {
return f; // selects int f(int)
};
auto p = static_cast<int(*)(int)>(f); // selects int f(int)
}
Tampoco se limita a inicializar un tipo concreto. También podría inferir solo del número de argumentos
y proporciona este ejemplo en vivo:
void overload(int, int) {}
void overload(int, int, int) {}
template <typename T1, typename T2,
typename A1, typename A2>
void f(void (*)(T1, T2), A1&&, A2&&) {}
template <typename T1, typename T2, typename T3,
typename A1, typename A2, typename A3>
void f(void (*)(T1, T2, T3), A1&&, A2&&, A3&&) {}
int main () {
f(&overload, 1, 2);
}
que elaboro un poco más aquí.
-
También podríamos describir esto como: ¿casos en los que el tipo de expresión depende del contexto?
–MM
12 de noviembre de 2018 a las 23:56
Creo en la conversión estática de funciones sobrecargadas, el flujo va en la dirección opuesta a la resolución de sobrecarga habitual. Así que uno de esos es al revés, supongo.
-
Creo que esto es correcto. Y es cuando pasas un nombre de función a un tipo de puntero de función; la información de tipo fluye desde el contexto de la expresión (el tipo que está asignando/construyendo/etc.) hacia atrás en el nombre de la función para determinar qué sobrecarga se elige.
– Yakk – Adam Nevraumont
12 de noviembre de 2018 a las 22:08
asignaciones de especialización parcial y desestructuración de coincidencia de patrones.
– v.oddou
13 de noviembre de 2018 a las 5:11