¿Cuándo fluye la información de tipos hacia atrás en C ++?


92

Acabo de ver a Stephan T. Lavavej hablar CppCon 2018sobre "Deducción de argumentos de plantilla de clase", donde en algún momento dice por cierto:

En el tipo de C ++ la información 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 refería, no se me ocurrió 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?


asignación de patrones de especialización parcial y desestructuración.
v.oddou

Respuestas:


80

Aquí hay al menos un caso:

struct foo {
  template<class T>
  operator T() const {
    std::cout << sizeof(T) << "\n";
    return {};
  }
};

si lo hace foo f; int x = f; double y = f;, la información de tipo fluirá "hacia atrás" para averiguar qué Thay dentro operator T.

Puede 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 hacerlo {1,2,3}? Bueno, {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, requieren un poco más de magia: ejemplo en vivo . (Tengo que hacer que la devolución de deduce haga una verificación SFINAE de F, luego haga que 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, ¡muchas gracias! Tuve que agregar una pauta de deducción de plantilla para compilar su ejemplo , pero aparte de eso, ¡funciona como un encanto!
Massimiliano

5
El &&calificador en el operator T()es un gran toque; ayuda a evitar la mala interacción con autoal causar un error de compilación si autose usa incorrectamente aquí.
Justin

1
Eso es muy impresionante, ¿podría señalarme alguna referencia / hablar sobre la idea en el ejemplo? o tal vez es original :) ...
llllllllll

3
@lili ¿Qué idea? Cuento 5: ¿Usar el operador T para deducir tipos de devolución? ¿Usar etiquetas para pasar el tipo deducido a una lambda? ¿Utiliza operadores de conversión para construir su propio objeto de ubicación? ¿Conectando los 4?
Yakk - Adam Nevraumont

1
@lili El ejemplo de la "forma más avanzada" es, como dije, solo 4 ideas juntas. Hice el encolado 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 razonablemente oscuras (como se queja Tootsie), pero nada nuevo.
Yakk - Adam Nevraumont

31

Stephan T.Lavavej explicó el caso del que estaba hablando en un tweet :

El caso en el que estaba pensando es en el que puede tomar la dirección de una función sobrecargada / con plantilla y si se está utilizando para inicializar una variable de un tipo específico, eso eliminará la ambigüedad de cuál desea. (Hay una lista de lo que desambigua).

podemos ver ejemplos de esto en la página cppreference en la dirección de la función sobrecargada , he 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)
}

Michael Park agrega :

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í .


4
También podríamos describir esto como: ¿casos en los que el tipo de expresión depende del contexto?
MM

20

Creo en la transmisión estática de funciones sobrecargadas, el flujo va en la dirección opuesta a la resolución de sobrecarga habitual. Uno de esos está al revés, supongo.


7
Creo que esto es correcto. Y es cuando pasa 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 le está asignando / construyendo / etc.) hacia atrás en el nombre de la función para determinar qué sobrecarga se elige.
Yakk - Adam Nevraumont
Al usar nuestro sitio, usted reconoce que ha leído y comprende nuestra Política de Cookies y Política de Privacidad.
Licensed under cc by-sa 3.0 with attribution required.