Quando as informações de tipo fluem para trás em C ++?


92

Acabei de assistir a palestra de Stephan T. Lavavej CppCon 2018em "Class Template Argument Deduction", onde em algum momento ele disse incidentalmente:

Em C ++, as informações de tipo quase nunca fluem para trás ... Eu tive que dizer "quase" porque há um ou dois casos, possivelmente mais, mas muito poucos .

Apesar de tentar descobrir a quais casos ele se referia, não consegui pensar em nada. Daí a pergunta:

Em quais casos o padrão C ++ 17 exige que as informações de tipo se propaguem para trás?


padrão de correspondência de especialização parcial e atribuições de desestruturação.
v.oddou

Respostas:


80

Aqui está pelo menos um caso:

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

se o fizer foo f; int x = f; double y = f;, as informações de tipo fluirão "para trás" para descobrir o que Testá dentro operator T.

Você pode usar isso de uma forma mais avançada:

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)... };
  }};
}

então agora eu posso fazer

std::vector<int> v = construct_from( 1, 2, 3 );

e funciona.

Claro, por que não fazer {1,2,3}? Bem, {1,2,3}não é uma expressão.

std::vector<std::vector<int>> v;
v.emplace_back( construct_from(1,2,3) );

que, reconhecidamente, exige um pouco mais de magia: exemplo ao vivo . (Eu tenho que fazer o deduzir retorno fazer uma verificação SFINAE de F, então fazer o F ser SFINAE amigável, e eu tenho que bloquear std :: initializer_list no operador deduce_return_t T.)


Resposta muito interessante, e aprendi um truque novo, muito obrigado! Eu tive que adicionar uma diretriz de dedução de modelo para fazer seu exemplo compilar , mas além disso, funciona perfeitamente!
Massimiliano

5
O &&qualificador no operator T()é um ótimo toque; ajuda a evitar a interação deficiente com auto, causando um erro de compilação se autofor mal utilizado aqui.
Justin

1
Isso é muito impressionante, você poderia me indicar alguma referência / falar sobre a ideia no exemplo? ou talvez seja original :) ...
llllllllll

3
@lili Qual ideia? Eu conto 5: Usando o operador T para deduzir os tipos de retorno? Usando tags para passar o tipo deduzido para um lambda? Usando operadores de conversão para criar seu próprio objeto de posicionamento? Conectando todos os 4?
Yakk - Adam Nevraumont

1
O exemplo de @lili Tha "mais avançado" é, como eu disse, apenas 4 ou mais idéias coladas. Fiz a colagem na hora para este post, mas certamente já vi muitos pares ou até trigêmeos desses usados ​​juntos. É um monte de técnicas razoavelmente obscuras (como o tootsie reclama), mas nada de novo.
Yakk - Adam Nevraumont

31

Stephan T. Lavavej explicou o caso que ele estava falando em um tweet :

O caso que eu estava pensando é onde você pode pegar o endereço de uma função sobrecarregada / modelada e se ela está sendo usada para inicializar uma variável de um tipo específico, isso irá desambiguar qual você deseja. (Há uma lista do que desambigua.)

podemos ver exemplos disso na página cppreference em Endereço da função sobrecarregada , exceto alguns abaixo:

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 adiciona :

Também não está limitado a inicializar um tipo concreto. Também pode inferir apenas a partir do número de argumentos

e fornece este exemplo ao 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 elaborei um pouco mais aqui .


4
Também poderíamos descrever isso como: casos em que o tipo de uma expressão depende do contexto?
MM

20

Acredito na conversão estática de funções sobrecarregadas, o fluxo vai na direção oposta, como na resolução de sobrecarga usual. Então, um desses está ao contrário, eu acho.


7
Eu acredito que isto está correto. E é quando você passa um nome de função para um tipo de ponteiro de função; as informações de tipo fluem do contexto da expressão (o tipo que você está atribuindo a / construindo / etc) de volta para o nome da função para determinar qual sobrecarga é escolhida.
Yakk - Adam Nevraumont
Ao utilizar nosso site, você reconhece que leu e compreendeu nossa Política de Cookies e nossa Política de Privacidade.
Licensed under cc by-sa 3.0 with attribution required.