Por que não inferir o parâmetro do modelo do construtor?

102

minha pergunta hoje é muito simples: por que o compilador não pode inferir parâmetros de modelo de construtores de classe, tanto quanto pode fazer a partir de parâmetros de função? Por exemplo, por que o código a seguir não poderia ser válido:

template<typename obj>
class Variable {
      obj data;
      public: Variable(obj d)
              {
                   data = d;
              }
};

int main()
{
    int num = 2;
    Variable var(num); //would be equivalent to Variable<int> var(num),
    return 0;          //but actually a compile error
}

Como eu disse, entendo que isso não é válido, então minha pergunta é por que não é? Permitir isso criaria grandes lacunas sintáticas? Existe uma instância em que não se desejaria essa funcionalidade (onde inferir um tipo causaria problemas)? Estou apenas tentando entender a lógica por trás da permissão de inferência de template para funções, mas não para classes construídas adequadamente.

GRB
fonte
Eu convidaria alguém (posso fazer isso, mas não agora), para compilar a resposta de Drahakar e Pitis (pelo menos) como bons contra-exemplos de porque isso não pode funcionar
jpinto3912
2
Observe também que isso é facilmente contornado viatemplate<class T> Variable<T> make_Variable(T&& p) {return Variable<T>(std::forward<T>(p));}
Mooing Duck
3
Você pode obter o que deseja var = Variable <decltype (n)> (n);
QuentinUK de
18
C ++ 17 permitirá isso! Esta proposta foi aceita: open-std.org/jtc1/sc22/wg21/docs/papers/2015/p0091r0.html
underscore_d
1
@underscore_d Excelente! Estava na hora! Pareceu natural para mim que é assim que deveria funcionar, e a fonte de irritação que não funcionou.
amdn

Respostas:

46

Acho que não é válido porque o construtor nem sempre é o único ponto de entrada da classe (estou falando sobre o construtor de cópia e operador =). Suponha que você esteja usando sua classe assim:

MyClass m(string s);
MyClass *pm;
*pm = m;

Não tenho certeza se seria tão óbvio para o analisador saber que tipo de modelo é MyClass pm;

Não tenho certeza se o que eu disse faz sentido, mas fique à vontade para adicionar algum comentário, essa é uma pergunta interessante.

C ++ 17

É aceito que C ++ 17 terá dedução de tipo a partir de argumentos do construtor.

Exemplos:

std::pair p(2, 4.5);
std::tuple t(4, 3, 2.5);

Artigo aceito .

Drahakar
fonte
8
Este é realmente um grande ponto que eu nunca considerei. Não vejo nenhuma maneira de contornar o fato de que o ponteiro teria que ser específico do tipo (ou seja, teria que ser MyClass <string> * pm). Se for esse o caso, tudo o que você acabará fazendo é evitar especificar o tipo na instanciação; alguns meros caracteres de trabalho extra (e apenas se o objeto for feito na pilha, não no heap, conforme acima). Sempre suspeitei que a inferência de classe pode abrir uma lata de vermes sintática, e acho que pode ser isso.
GRB
2
Não vejo bem como permitir a inferência de parâmetro de modelo de construtores exigiria a permissão de declarações não especializadas sem chamadas de construtor, como em sua segunda linha. Ou seja, MyClass *pmaqui seria inválido pelo mesmo motivo que uma função declarada template <typename T> void foo();não pode ser chamada sem especialização explícita.
Kyle Strand
3
@KyleStrand Sim, ao dizer 'os argumentos do modelo de classe não podem ser deduzidos de seus construtores porque [exemplo que não usa nenhum construtor] ', essa resposta é completamente irrelevante. Eu realmente não posso acreditar que foi aceito, alcancei +29, levei 6 anos para alguém perceber o problema gritante e sentei sem um único downvote por 7 anos. Ninguém mais pensa enquanto lêem, ou ...?
sublinhado_d
1
@underscore_d Gosto de como, como está atualmente, esta resposta diz "pode ​​haver alguns problemas com esta proposta; não tenho certeza se o que acabei de dizer faz sentido (!), sinta-se à vontade para comentar (!!); e ah, a propósito, é exatamente assim que o C ++ 17 funcionará. "
Kyle Strand de
1
@KyleStrand Ah sim, esse é outro problema, que percebi, mas esqueci de mencionar entre todas as outras coisas divertidas. A edição sobre C ++ 17 não era do OP ... e IMO não deveria ter sido aprovada, mas postada como uma nova resposta: teria sido recusada como 'muda o significado da postagem' mesmo se a postagem tivesse não fez sentido para começar ... Eu não sabia que editar em seções inteiramente novas era um jogo justo e certamente teve edições menos drásticas rejeitadas, mas eu acho que é a sorte do sorteio em termos de quais revisores você recebe.
sublinhado_d
27

Você não pode fazer o que pede por motivos que outras pessoas abordaram, mas você pode fazer o seguinte:

template<typename T>
class Variable {
    public: Variable(T d) {}
};
template<typename T>
Variable<T> make_variable(T instance) {
  return Variable<T>(instance);
}

que para todos os efeitos e propósitos é a mesma coisa que você pede. Se você adora encapsulamento, pode tornar make_variable uma função de membro estático. Isso é o que as pessoas chamam de construtor nomeado. Portanto, ele não apenas faz o que você deseja, mas é quase chamado do que você deseja: o compilador está inferindo o parâmetro do modelo a partir do construtor (nomeado).

NB: qualquer compilador razoável irá otimizar o objeto temporário quando você escrever algo como

auto v = make_variable(instance);
Lionel
fonte
6
Gostaria de salientar que não é particularmente útil tornar a função membro estático nesse caso, porque para isso você teria que especificar o argumento do template para uma classe para chamá-lo de qualquer maneira, então não haveria nenhum ponto em deduzi-lo.
Predelnik
3
E ainda melhor em C ++ 11, você pode fazer auto v = make_variable(instance)isso sem realmente precisar especificar o tipo
Claudiu
1
Sim, lol com a ideia de declarar a função make como um staticmembro ... pense nisso por apenas um segundo. Deixando isso de lado: as funções make livres foram de fato a solução, mas é um monte de boilerplate redundante, enquanto você está digitando, você sabe que não deveria, porque o compilador tem acesso a todas as informações que você está repetindo. .. e felizmente C ++ 17 canoniza isso.
sublinhado_d
21

Na era iluminada de 2016, com dois novos padrões em nosso currículo desde que essa pergunta foi feita e um novo chegando, o crucial é saber que os compiladores que suportam o padrão C ++ 17 compilarão seu código como está .

Dedução de argumento de modelo para modelos de classe em C ++ 17

Aqui (cortesia de uma edição de Olzhas Zhumabek da resposta aceita) está o artigo detalhando as mudanças relevantes no padrão.

Lidando com as preocupações de outras respostas

A resposta com melhor classificação atual

Esta resposta indica que "construtor de cópia e operator=" não saberia as especializações de modelo corretas.

Isso é um absurdo, porque o construtor de cópia padrão operator= existe apenas para um tipo de modelo conhecido :

template <typename T>
class MyClass {
    MyClass(const MyClass&) =default;
    ... etc...
};

// usage example modified from the answer
MyClass m(string("blah blah blah"));
MyClass *pm;   // WHAT IS THIS?
*pm = m;

Aqui, como observei nos comentários, não há razão para MyClass *pmser uma declaração legal com ou sem a nova forma de inferência: MyClass não é um tipo (é um modelo), portanto, não faz sentido declarar um ponteiro de tipo MyClass. Esta é uma maneira possível de corrigir o exemplo:

MyClass m(string("blah blah blah"));
decltype(m) *pm;               // uses type inference!
*pm = m;

Aqui, pm é do tipo correto e, portanto, a inferência é trivial. Além disso, é impossível misturar tipos acidentalmente ao chamar o construtor de cópia:

MyClass m(string("blah blah blah"));
auto pm = &(MyClass(m));

Aqui, pmhaverá um ponteiro para uma cópia de m. Aqui, MyClassestá sendo construído a partir de m- que é do tipo MyClass<string>(e não do tipo inexistente MyClass). Assim, no ponto em que pm's tipo é inferido, não é suficiente informação para saber que o tipo de molde m, e portanto, o tipo de modelo de pm, é string.

Além disso, o seguinte sempre gerará um erro de compilação :

MyClass s(string("blah blah blah"));
MyClass i(3);
i = s;

Isso ocorre porque a declaração do construtor de cópia não é modelada:

MyClass(const MyClass&);

Aqui, o tipo de modelo do argumento do construtor de cópia corresponde ao tipo de modelo da classe geral; isto é, quando MyClass<string>é instanciado, MyClass<string>::MyClass(const MyClass<string>&);é instanciado com ele, e quando MyClass<int>é instanciado, MyClass<int>::MyClass(const MyClass<int>&);é instanciado. A menos que seja explicitamente especificado ou um construtor com modelo seja declarado, não há razão para o compilador instanciar MyClass<int>::MyClass(const MyClass<string>&);, o que obviamente seria inapropriado.

A resposta de Cătălin Pitiș

Pitiș dá um exemplo deduzindo Variable<int>e Variable<double>, em seguida, afirma:

Eu tenho o mesmo nome de tipo (Variável) no código para dois tipos diferentes (Variável e Variável). Do meu ponto de vista subjetivo, isso afeta muito a legibilidade do código.

Conforme observado no exemplo anterior, Variableele próprio não é um nome de tipo, embora o novo recurso faça com que ele se pareça sintaticamente.

Pitiș então pergunta o que aconteceria se nenhum construtor fosse fornecido que permitiria a inferência apropriada. A resposta é que nenhuma inferência é permitida, porque a inferência é disparada pela chamada do construtor . Sem uma chamada de construtor, não há inferência .

Isso é semelhante a perguntar qual versão de fooé deduzida aqui:

template <typename T> foo();
foo();

A resposta é que esse código é ilegal, pelo motivo declarado.

Resposta de MSalter

Esta é, pelo que eu posso dizer, a única resposta que levanta uma preocupação legítima sobre o recurso proposto.

O exemplo é:

Variable var(num);  // If equivalent to Variable<int> var(num),
Variable var2(var); // Variable<int> or Variable<Variable<int>> ?

A questão principal é: o compilador seleciona o construtor inferido por tipo aqui ou o construtor de cópia ?

Experimentando o código, podemos ver que o construtor de cópia está selecionado. Para expandir o exemplo :

Variable var(num);          // infering ctor
Variable var2(var);         // copy ctor
Variable var3(move(var));   // move ctor
// Variable var4(Variable(num));     // compiler error

Não tenho certeza de como a proposta e a nova versão da norma especificam isso; parece ser determinado por "guias de dedução", que são um novo padrão de linguagem que ainda não entendo.

Também não sei por que a var4dedução é ilegal; o erro do compilador de g ++ parece indicar que a instrução está sendo analisada como uma declaração de função.

Kyle Strand
fonte
Que resposta ótima e detalhada! var4é apenas um caso de "análise mais irritante" (não relacionado à dedução do argumento do modelo). Costumávamos usar apenas parênteses extras para isso, mas atualmente acho que usar colchetes para denotar construção sem ambigüidades é o conselho usual.
Sumudu Fernando
@SumuduFernando Obrigado! Você quer dizer que Variable var4(Variable(num));é tratado como uma declaração de função? Em caso afirmativo, por que Variable(num)uma especificação de parâmetro válida?
Kyle Strand
@SumuduFernando Não se preocupe, eu não tinha ideia de que isso era válido: coliru.stacked-crooked.com/a/98c36b8082660941
Kyle Strand
11

Ainda ausente: torna o código a seguir bastante ambíguo:

int main()
{
    int num = 2;
    Variable var(num);  // If equivalent to Variable<int> var(num),
    Variable var2(var); //Variable<int> or Variable<Variable<int>> ?
}
MSalters
fonte
Outro bom ponto. Assumindo que existe uma Variável definida pelo construtor de cópias (Variável <obj> d), deveria haver algum tipo de precedência estabelecida.
GRB
1
Ou, alternativamente, faça com que o compilador lance um erro de parâmetro de modelo indefinido novamente, bem como sugeri em relação à resposta de Pitis. No entanto, se você seguir esse caminho, o número de vezes em que a inferência pode acontecer sem problemas (erros) está ficando cada vez menor.
GRB
Na verdade, esse é um ponto interessante e (como observei em minha resposta) ainda não tenho certeza de como a proposta C ++ 17 aceita resolve isso.
Kyle Strand de
9

Supondo que o compilador suporte o que você pediu. Então este código é válido:

Variable v1( 10); // Variable<int>

// Some code here

Variable v2( 20.4); // Variable<double>

Agora, eu tenho o mesmo nome de tipo (Variável) no código para dois tipos diferentes (Variável e Variável). Do meu ponto de vista subjetivo, isso afeta muito a legibilidade do código. Ter o mesmo nome de tipo para dois tipos diferentes no mesmo namespace parece enganoso para mim.

Atualização posterior: Outra coisa a considerar: especialização de modelo parcial (ou total).

E se eu me especializar em Variável e não fornecer nenhum construtor como você espera?

Então eu teria:

template<>
class Variable<int>
{
// Provide default constructor only.
};

Então eu tenho o código:

Variable v( 10);

O que o compilador deve fazer? Use a definição de classe de Variável genérica para deduzir que é Variável e descubra que Variável não fornece um construtor de parâmetro.

Cătălin Pitiș
fonte
1
Pior: e se você tiver apenas Variable <int> :: Variable (float)? Agora você tem duas maneiras de deduzir a Variável (1f) e nenhuma maneira de deduzir a Variável (1).
MSalters
É um bom ponto, mas poderia ser facilmente superado lançando: Variável v1 ((duplo) 10)
jpinto3912
Concordo que a legibilidade do código é uma questão subjetiva, no entanto, concordo 100% com o que você está dizendo sobre a especialização de modelo. A solução provavelmente seria dar um erro de parâmetro de modelo indefinido (uma vez que o compilador olha para a especialização <int> e não vê nenhum construtor válido, diga que não tem ideia de qual modelo você deseja usar e que você deve especificar explicitamente), mas Eu concordo que não é uma solução bonita. Eu acrescentaria isso como outra grande lacuna sintática que precisaria ser tratada (mas poderia ser resolvida se aceitarmos as consequências).
GRB
4
@ jpinto3912 - você está perdendo o ponto. O compilador tem que instanciar TODAS as variáveis ​​possíveis <T> para verificar se QUALQUER variável de ctor <T> :: Variable fornece um ctor ambíguo. Livrar-se da ambigüidade não é o problema - basta instanciar a Variável <duplo> você mesmo, se é isso que você deseja. É encontrar essa ambigüidade em primeiro lugar que torna isso impossível.
MSalters
6

Os padrões C ++ 03 e C ++ 11 não permitem a dedução do argumento do modelo a partir dos parâmetros passados ​​para o construtor.

Mas existe uma proposta para "Dedução de parâmetros de modelo para construtores", então você pode obter o que está pedindo em breve. Editar: na verdade, esse recurso foi confirmado para C ++ 17.

Consulte: http://www.open-std.org/jtc1/sc22/wg21/docs/papers/2013/n3602.html e http://www.open-std.org/jtc1/sc22/wg21/docs/ papers / 2015 / p0091r0.html

ChetS
fonte
O recurso foi adicionado ao C ++ 17, mas não se "em breve" se aplicar a um período de 6 a 8 anos. ;)
ChetS de
2

Muitas classes não dependem dos parâmetros do construtor. Existem apenas algumas classes que têm apenas um construtor e parametrizam com base no (s) tipo (s) deste (s) construtor (es).

Se você realmente precisa de inferência de modelo, use uma função auxiliar:

template<typename obj>
class Variable 
{
      obj data;
public: 
      Variable(obj d)
      : data(d)
      { }
};

template<typename obj>
inline Variable<obj> makeVariable(const obj& d)
{
    return Variable<obj>(d);
}
rlbond
fonte
1
É claro que essa funcionalidade só seria útil para algumas classes, mas o mesmo pode ser dito para a inferência de funções. Nem todas as funções modeladas obtêm seus parâmetros da lista de argumentos, mas permitimos inferência para as funções que o fazem.
GRB
1

A dedução de tipos é limitada às funções de modelo no C ++ atual, mas há muito se percebeu que a dedução de tipos em outros contextos seria muito útil. Conseqüentemente, C ++ 0x's auto.

Embora exatamente o que você sugere não seja possível em C ++ 0x, o seguinte mostra que você pode chegar bem perto:

template <class X>
Variable<typename std::remove_reference<X>::type> MakeVariable(X&& x)
{
    // remove reference required for the case that x is an lvalue
    return Variable<typename std::remove_reference<X>::type>(std::forward(x));
}

void test()
{
    auto v = MakeVariable(2); // v is of type Variable<int>
}
James Hopkin
fonte
0

Você está certo, o compilador poderia adivinhar facilmente, mas não está no padrão ou C ++ 0x até onde eu sei, então você terá que esperar pelo menos mais 10 anos (padrões ISO taxa fixa de retorno) antes que os fornecedores de compiladores adicionem este recurso

Robert Gould
fonte
Isso não está correto com o próximo padrão, uma palavra-chave automática será introduzida. Dê uma olhada na postagem de James Hopkins neste tópico. stackoverflow.com/questions/984394/… . Ele mostra como isso será possível em C ++ 0x.
ovanes
1
Só para me corrigir, a palavra-chave auto também está presente no padrão atual, mas com o propósito diferente.
ovanes
Parece que serão 8 anos (a partir do momento desta resposta) ... então, 10 anos não foi um palpite ruim, embora tenha havido dois padrões nesse meio tempo!
Kyle Strand de
-1

Vejamos o problema com referência a uma classe com a qual todos deveriam estar familiarizados - std :: vector.

Em primeiro lugar, um uso muito comum de vetor é usar o construtor que não leva parâmetros:

vector <int> v;

Nesse caso, obviamente, nenhuma inferência pode ser realizada.

Um segundo uso comum é criar um vetor pré-dimensionado:

vector <string> v(100);

Aqui, se a inferência fosse usada:

vector v(100);

obtemos um vetor de ints, não de strings, e presumivelmente não é dimensionado!

Por último, considere os construtores que usam vários parâmetros - com "inferência":

vector v( 100, foobar() );      // foobar is some class

Qual parâmetro deve ser usado para inferência? Precisaríamos de alguma forma de dizer ao compilador que deveria ser o segundo.

Com todos esses problemas para uma classe tão simples quanto vetorial, é fácil ver por que a inferência não é usada.


fonte
3
Acho que você está entendendo mal a ideia. A inferência de tipo para construtores ocorreria apenas SE o tipo de modelo fizer parte do construtor. Suponha que o vetor tenha o modelo de definição de modelo <nome do tipo T>. Seu exemplo não é um problema porque o construtor do vetor seria definido como vetor (tamanho interno), não vetor (tamanho T). Apenas no caso do vetor (tamanho T) ocorreria alguma inferência; no primeiro exemplo, o compilador daria um erro dizendo que T é indefinido. Essencialmente idêntico ao funcionamento da inferência do template de função.
GRB
Portanto, isso ocorreria apenas para construtores que têm um único parâmetro e onde esse parâmetro é um tipo de parâmetro de modelo? Isso parece um número cada vez menor de casos.
Não precisa ser necessariamente um único parâmetro. Por exemplo, pode-se ter um construtor de vetor de vetor (int size, T firstElement). Se um modelo tiver vários parâmetros (modelo <nome de tipo T, nome de tipo U>), um poderia ter Holder :: Holder (T firstObject, U secondObject). Se um modelo tem vários parâmetros, mas o construtor usa apenas um deles, por exemplo, Holder (U secondObject), então T sempre teria que ser declarado explicitamente. As regras deveriam ser tão semelhantes quanto possível à inferência do template de função.
GRB
-2

Fazendo do ctor um modelo, a variável pode ter apenas uma forma, mas vários ctors:

class Variable {
      obj data; // let the compiler guess
      public:
      template<typename obj>
      Variable(obj d)
       {
           data = d;
       }
};

int main()
{
    int num = 2;
    Variable var(num);  // Variable::data int?

    float num2 = 2.0f;
    Variable var2(num2);  // Variable::data float?
    return 0;         
}

Vejo? Não podemos ter vários membros Variable :: data.

Nick Dandoulakis
fonte
Isso não faria sentido em nenhum cenário. obj em termos de dados obj é indefinido, uma vez que essa classe não é mais um modelo. Esse código seria inválido de qualquer maneira.
GRB
Eu queria o comportamento do compilador que você descreveu, então descobri uma maneira de contornar essa restrição (no meu caso), que você pode achar interessante, stackoverflow.com/questions/228620/garbage-collection-in-c-why/…
Nick Dandoulakis
-2

Consulte The C ++ Template Argument Deduction para obter mais informações sobre isso.

Igor Krivokon
fonte
4
Eu li este artigo antes e não parece falar muito sobre o que estou dizendo. A única vez que o escritor parece falar sobre dedução de argumento com relação às classes é quando ele diz que isso não pode ser feito no início do artigo;) - se você pudesse apontar as seções que você acha que são relevantes, embora eu ' Eu realmente aprecio isso.
GRB