Modelos C ++ que aceitam apenas certos tipos

159

Em Java, você pode definir classe genérica que aceita apenas tipos que estendem a classe de sua escolha, por exemplo:

public class ObservableList<T extends List> {
  ...
}

Isso é feito usando a palavra-chave "extends".

Existe algum equivalente simples a essa palavra-chave em C ++?

mgamer
fonte
Uma pergunta bastante antiga ... Sinto que o que está faltando aqui (também nas respostas) é que os genéricos Java não são realmente equivalentes aos modelos em C ++. Há semelhanças, mas IMHO deve-se ter cuidado com a tradução diretamente uma solução Java para C ++ apenas para perceber que eles são talvez feita para diferentes tipos de problemas;)
idclev 463035818

Respostas:

104

Sugiro usar o recurso de declaração estática do Boost em conjunto com is_base_ofa biblioteca Boost Type Traits:

template<typename T>
class ObservableList {
    BOOST_STATIC_ASSERT((is_base_of<List, T>::value)); //Yes, the double parentheses are needed, otherwise the comma will be seen as macro argument separator
    ...
};

Em alguns outros casos mais simples, você pode simplesmente declarar um modelo global adiante, mas apenas defini-lo (explicitamente ou parcialmente) para os tipos válidos:

template<typename T> class my_template;     // Declare, but don't define

// int is a valid type
template<> class my_template<int> {
    ...
};

// All pointer types are valid
template<typename T> class my_template<T*> {
    ...
};

// All other types are invalid, and will cause linker error messages.

[EDIT Menor 12/12/2013: o uso de um modelo declarado, mas não definido, resultará em mensagens de erro do vinculador , não do compilador.]

j_random_hacker
fonte
Afirmações estáticas também são boas. :)
macbirdie
5
@ John: Receio que a especialização corresponda myBaseTypeexatamente. Antes de descartar o Boost, você deve saber que a maioria é de código de modelo apenas de cabeçalho - portanto, não há memória ou tempo em tempo de execução para itens que você não usa. Além disso, as coisas específicas que você usaria aqui ( BOOST_STATIC_ASSERT()e is_base_of<>) podem ser implementadas usando apenas declarações (ou seja, nenhuma definição real de funções ou variáveis), para que não ocupem espaço ou tempo.
Jrandom_hacker
50
C ++ 11 chegou. Agora podemos usar static_assert(std::is_base_of<List, T>::value, "T must extend list").
Siyuan Ren 10/09/13
2
BTW, a razão pela qual o parêntese duplo é necessário é que BOOST_STATIC_ASSERT é uma macro e o parêntese extra impede o pré-processador de interpretar a vírgula nos argumentos da função is_base_of como um segundo argumento de macro.
Jritz42
1
@Andreyua: Eu realmente não entendo o que está faltando. Você pode tentar declarar uma variável my_template<int> x;oue my_template<float**> y;verificar se o compilador permite e, em seguida, declarar uma variável my_template<char> z;e verificar se não.
Jrandom_hacker
134

Isso normalmente não é garantido em C ++, como outras respostas aqui observadas. Em C ++, tendemos a definir tipos genéricos com base em outras restrições além de "herda desta classe". Se você realmente queria fazer isso, é muito fácil fazer no C ++ 11 e <type_traits>:

#include <type_traits>

template<typename T>
class observable_list {
    static_assert(std::is_base_of<list, T>::value, "T must inherit from list");
    // code here..
};

Isso quebra muitos dos conceitos que as pessoas esperam em C ++. É melhor usar truques como definir suas próprias características. Por exemplo, talvez observable_listqueira aceitar qualquer tipo de recipiente que tem os typedefs const_iteratore um begine endfunção membro que retorna const_iterator. Se você restringir isso às classes que herdam list, um usuário que tenha seu próprio tipo que não herda, listmas fornece essas funções de membro e typedefs, não poderá usar o seu observable_list.

Existem duas soluções para esse problema, uma delas é não restringir nada e confiar na digitação do pato. Uma grande desvantagem dessa solução é que ela envolve uma quantidade enorme de erros que podem ser difíceis para os usuários entenderem. Outra solução é definir características para restringir o tipo fornecido para atender aos requisitos da interface. A grande desvantagem dessa solução é que envolve reduções extras que podem ser vistas como irritantes. No entanto, o lado positivo é que você poderá escrever suas próprias mensagens de erro à la static_assert.

Para completar, a solução para o exemplo acima é fornecida:

#include <type_traits>

template<typename...>
struct void_ {
    using type = void;
};

template<typename... Args>
using Void = typename void_<Args...>::type;

template<typename T, typename = void>
struct has_const_iterator : std::false_type {};

template<typename T>
struct has_const_iterator<T, Void<typename T::const_iterator>> : std::true_type {};

struct has_begin_end_impl {
    template<typename T, typename Begin = decltype(std::declval<const T&>().begin()),
                         typename End   = decltype(std::declval<const T&>().end())>
    static std::true_type test(int);
    template<typename...>
    static std::false_type test(...);
};

template<typename T>
struct has_begin_end : decltype(has_begin_end_impl::test<T>(0)) {};

template<typename T>
class observable_list {
    static_assert(has_const_iterator<T>::value, "Must have a const_iterator typedef");
    static_assert(has_begin_end<T>::value, "Must have begin and end member functions");
    // code here...
};

Existem muitos conceitos mostrados no exemplo acima que mostram os recursos do C ++ 11. Alguns termos de pesquisa para os curiosos são modelos variados, SFINAE, expressão SFINAE e características de tipo.

Rapptz
fonte
2
Eu nunca percebi que os modelos C ++ usam digitação de pato até hoje. Meio bizarro!
Andy
2
Dadas as extensas restrições de política que o C ++ introduziu no C , não sei por que template<class T:list>esse conceito é ofensivo. Obrigado pela dica.
BVJ
61

A solução simples, que ninguém mencionou ainda, é simplesmente ignorar o problema. Se eu tentar usar um inttipo de modelo em um modelo de função que espera uma classe de contêiner como vetor ou lista, receberei um erro de compilação. Bruto e simples, mas resolve o problema. O compilador tentará usar o tipo que você especificar e, se isso falhar, gera um erro de compilação.

O único problema é que as mensagens de erro que você recebe serão difíceis de ler. No entanto, é uma maneira muito comum de fazer isso. A biblioteca padrão está cheia de modelos de função ou classe que esperam determinado comportamento do tipo de modelo e não fazem nada para verificar se os tipos usados ​​são válidos.

Se você deseja mensagens de erro mais agradáveis ​​(ou se deseja capturar casos que não produziriam um erro de compilador, mas ainda não fazem sentido), você pode, dependendo da complexidade que deseja criar, use a declaração estática do Boost ou a biblioteca Boost concept_check.

Com um compilador atualizado, você tem um built_in static_assert, que pode ser usado.

jalf
fonte
7
Sim, sempre achei que os modelos são a coisa mais próxima da digitação em C ++. Se tiver todos os elementos necessários para um modelo, ele poderá ser usado em um modelo.
@ John: me desculpe, eu não posso fazer cabeça ou coroa disso. Qual é o tipo Te de onde esse código é chamado? Sem algum contexto, não tenho chance de entender esse trecho de código. Mas o que eu disse é verdade. Se você tentar chamar toString()um tipo que não possui uma toStringfunção de membro, receberá um erro de compilação.
achou
@ John: da próxima vez, talvez você deve ser um pouco menos no gatilho downvoting pessoas quando o problema está no seu código
jalf
@ jalf, ok. +1. Essa foi uma ótima resposta, apenas tentando torná-la a melhor. Desculpe por interpretar mal. Eu pensei que estávamos falando sobre o uso do tipo como parâmetro para classes, não para modelos de função, que eu suponho que sejam membros do primeiro, mas que precisam chamar o compilador para sinalizar.
John
13

Podemos usar std::is_base_ofe std::enable_if:
( static_assertpodem ser removidos, as classes acima podem ser implementadas sob medida ou usadas a partir do boost se não pudermos fazer referência type_traits)

#include <type_traits>
#include <list>

class Base {};
class Derived: public Base {};

#if 0   // wrapper
template <class T> class MyClass /* where T:Base */ {
private:
    static_assert(std::is_base_of<Base, T>::value, "T is not derived from Base");
    typename std::enable_if<std::is_base_of<Base, T>::value, T>::type inner;
};
#elif 0 // base class
template <class T> class MyClass: /* where T:Base */
    protected std::enable_if<std::is_base_of<Base, T>::value, T>::type {
private:
    static_assert(std::is_base_of<Base, T>::value, "T is not derived from Base");
};
#elif 1 // list-of
template <class T> class MyClass /* where T:list<Base> */ {
    static_assert(std::is_base_of<Base, typename T::value_type>::value , "T::value_type is not derived from Base");
    typedef typename std::enable_if<std::is_base_of<Base, typename T::value_type>::value, T>::type base; 
    typedef typename std::enable_if<std::is_base_of<Base, typename T::value_type>::value, T>::type::value_type value_type;

};
#endif

int main() {
#if 0   // wrapper or base-class
    MyClass<Derived> derived;
    MyClass<Base> base;
//  error:
    MyClass<int> wrong;
#elif 1 // list-of
    MyClass<std::list<Derived>> derived;
    MyClass<std::list<Base>> base;
//  error:
    MyClass<std::list<int>> wrong;
#endif
//  all of the static_asserts if not commented out
//  or "error: no type named ‘type’ in ‘struct std::enable_if<false, ...>’ pointing to:
//  1. inner
//  2. MyClass
//  3. base + value_type
}
firda
fonte
13

Tanto quanto sei, atualmente não é possível em C ++. No entanto, há planos de adicionar um recurso chamado "conceitos" no novo padrão C ++ 0x que forneça a funcionalidade que você está procurando. Este artigo da Wikipedia sobre conceitos de C ++ explicará mais detalhadamente.

Sei que isso não resolve o seu problema imediato, mas existem alguns compiladores C ++ que já começaram a adicionar recursos do novo padrão; portanto, talvez seja possível encontrar um compilador que já tenha implementado o recurso de conceitos.

Barry Carr
fonte
4
Infelizmente, os conceitos foram retirados do padrão.
24909 macbirdie
4
Restrições e conceitos devem ser adotados para C ++ 20.
Petr Javorik
É possível mesmo sem conceitos, usando static_asserte SFINAE, como mostram as outras respostas. O problema restante para alguém vindo de Java ou C # ou Haskell (...) é que o compilador C ++ 20 não faz verificação de definição com relação aos conceitos necessários, como Java e C # fazem.
user7610
10

Acho que todas as respostas anteriores perderam de vista a floresta para as árvores.

Os genéricos Java não são iguais aos modelos ; eles usam apagamento de tipo , que é uma técnica dinâmica , em vez de compilar o polimorfismo em tempo , que é uma técnica estática . Deveria ser óbvio por que essas duas táticas muito diferentes não se adaptam bem.

Em vez de tentar usar uma construção de tempo de compilação para simular uma de execução, vejamos o que extendsrealmente faz: de acordo com o Stack Overflow e a Wikipedia , extends é usado para indicar subclassificação.

C ++ também suporta subclassificação.

Você também mostra uma classe de contêiner, que está usando apagamento de tipo na forma de um genérico, e se estende para executar uma verificação de tipo. No C ++, você deve executar o tipo de mecanismo de apagamento, o que é simples: faça um ponteiro para a superclasse.

Vamos envolvê-lo em um typedef, para facilitar o uso, em vez de criar uma classe inteira, et voila:

typedef std::list<superclass*> subclasses_of_superclass_only_list;

Por exemplo:

class Shape { };
class Triangle : public Shape { };

typedef std::list<Shape*> only_shapes_list;
only_shapes_list shapes;

shapes.push_back(new Triangle()); // Works, triangle is kind of shape
shapes.push_back(new int(30)); // Error, int's are not shapes

Agora, parece que List é uma interface, representando um tipo de coleção. Uma interface em C ++ seria apenas uma classe abstrata, ou seja, uma classe que implementa nada além de métodos virtuais puros. Usando esse método, você pode implementar facilmente seu exemplo de java em C ++, sem qualquer conceito ou especialização de modelo. Ele também teria um desempenho tão lento quanto os genéricos do estilo Java devido às pesquisas da tabela virtual, mas isso geralmente pode ser uma perda aceitável.

Alice
fonte
3
Não sou fã de respostas que usam frases como "deveria ser óbvio" ou "todo mundo sabe" e depois explica o que é óbvio ou universalmente conhecido. Óbvio é relativo ao contexto, experiência e contexto de experiência. Tais declarações são inerentemente rudes.
precisa saber é o seguinte
2
@DavidLively É cerca de dois anos tarde demais para criticar esta resposta por etiqueta, mas também discordo de você neste caso específico; Expliquei por que as duas técnicas não combinam antes de afirmar que era óbvio, não depois. Forneci o contexto e depois disse que a conclusão desse contexto era óbvia. Isso não se encaixa exatamente no seu molde.
Alice
O autor desta resposta disse que algo era óbvio depois de fazer algum trabalho pesado. Eu não acho que o autor pretendeu dizer que a solução era óbvia.
Luke Gehorsam
10

Um equivalente que aceita apenas tipos T derivados do tipo Lista se parece com

template<typename T, 
         typename std::enable_if<std::is_base_of<List, T>::value>::type* = nullptr>
class ObservableList
{
    // ...
};
nh_
fonte
8

Resumo executivo: não faça isso.

A resposta de j_random_hacker diz como fazer isso. No entanto, também gostaria de salientar que você não deve fazer isso. O ponto principal dos modelos é que eles podem aceitar qualquer tipo compatível, e as restrições de tipo de estilo Java quebram isso.

As restrições de tipo do Java são um bug, não um recurso. Eles estão lá porque o Java digita apagamento em genéricos, portanto, o Java não consegue descobrir como chamar métodos com base apenas no valor dos parâmetros de tipo.

C ++, por outro lado, não tem essa restrição. Os tipos de parâmetro do modelo podem ser de qualquer tipo compatível com as operações com as quais são usados. Não precisa haver uma classe base comum. Isso é semelhante ao "Duck Typing" do Python, mas feito em tempo de compilação.

Um exemplo simples mostrando o poder dos modelos:

// Sum a vector of some type.
// Example:
// int total = sum({1,2,3,4,5});
template <typename T>
T sum(const vector<T>& vec) {
    T total = T();
    for (const T& x : vec) {
        total += x;
    }
    return total;
}

Essa função de soma pode somar um vetor de qualquer tipo que suporte as operações corretas. Ele funciona com ambas as primitivas, como int / long / float / double, e tipos numéricos definidos pelo usuário que sobrecarregam o operador + =. Heck, você pode até usar esta função para juntar strings, já que elas suportam + =.

Nenhum boxe / unboxing de primitivos é necessário.

Observe que ele também constrói novas instâncias de T usando T (). Isso é trivial no C ++ usando interfaces implícitas, mas não é realmente possível em Java com restrições de tipo.

Embora os modelos C ++ não tenham restrições de tipo explícitas, eles ainda são do tipo seguro e não serão compilados com código que não suporta as operações corretas.

catphive
fonte
2
Se você está sugerindo modelos nunca especializados, também pode explicar por que ele está no idioma?
1
Entendi, mas se o argumento do modelo precisar ser derivado de um tipo específico, é melhor ter uma mensagem fácil de interpretar do static_assert do que o vômito normal do erro do compilador.
precisa saber é o seguinte
1
Sim, o C ++ é mais expressivo aqui, mas, embora geralmente seja uma coisa boa (porque podemos expressar mais com menos), às vezes queremos limitar deliberadamente o poder que damos a nós mesmos, para obter certeza de que entendemos completamente um sistema.
Jrandom_hacker
A especialização do tipo @Curg é útil quando você deseja tirar proveito de algo que só pode ser feito para determinados tipos. por exemplo, um booleano é ~ normalmente ~ um byte cada, mesmo que um byte possa ~ normalmente ~ conter 8 bits / booleanos; uma classe de coleção de modelos pode (e no caso de std :: map) se especializar em booleano, para que ele possa compactar os dados com mais rigidez para economizar memória.
Thecoshman
Além disso, para esclarecer, esta resposta não é "nunca se especialize em modelos", mas sim não use esse recurso para tentar limitar quais tipos podem ser usados ​​com um modelo.
Thecoshman
6

Isso não é possível em C ++ simples, mas você pode verificar os parâmetros do modelo em tempo de compilação através da Verificação de Conceito, por exemplo, usando o BCCL do Boost .

A partir do C ++ 20, os conceitos estão se tornando um recurso oficial da linguagem.

macbirdie
fonte
2
Bem, é possível, mas a verificação de conceito ainda é uma boa ideia. :)
j_random_hacker
Na verdade, eu quis dizer que não era possível em C ++ "simples". ;) #
28129 macbirdie
5
class Base
{
    struct FooSecurity{};
};

template<class Type>
class Foo
{
    typename Type::FooSecurity If_You_Are_Reading_This_You_Tried_To_Create_An_Instance_Of_Foo_For_An_Invalid_Type;
};

Verifique se as classes derivadas herdam a estrutura FooSecurity e se o compilador ficará chateado nos lugares certos.

Stuart
fonte
@Zehelvion Type::FooSecurityé usado na classe de modelo. Se a classe, passada no argumento do modelo, não tiver FooSecurity, a tentativa de usá-lo causa um erro. É certo que, se a classe passada no argumento do modelo não possui FooSecurity, ela não é derivada Base.
GingerPlusPlus
2

Uso do conceito C ++ 20

https://en.cppreference.com/w/cpp/language/constraints cppreference está fornecendo o caso de uso de herança como um exemplo explícito de conceito:

template <class T, class U>
concept Derived = std::is_base_of<U, T>::value;
 
template<Derived<Base> T>
void f(T);  // T is constrained by Derived<T, Base>

Para várias bases, acho que a sintaxe será:

template <class T, class U, class V>
concept Derived = std::is_base_of<U, T>::value || std::is_base_of<V, T>::value;
 
template<Derived<Base1, Base2> T>
void f(T);

Parece que o GCC 10 o implementou: https://gcc.gnu.org/gcc-10/changes.html e você pode obtê-lo como um PPA no Ubuntu 20.04 . https://godbolt.org/ Meu GCC 10.1 local ainda não reconheceu concept, portanto, não tenho certeza do que está acontecendo.

Ciro Santilli adicionou uma nova foto
fonte
1

Existe algum equivalente simples a essa palavra-chave em C ++?

Não.

Dependendo do que você está tentando realizar, pode haver substitutos adequados (ou até melhores).

Eu olhei através de algum código STL (no Linux, acho que é o derivado da implementação da SGI). Possui "afirmações conceituais"; por exemplo, se você precisar de um tipo que entenda *xe ++x, a asserção de conceito conteria esse código em uma função do-nothing (ou algo semelhante). Requer alguma sobrecarga, portanto, pode ser inteligente colocá-lo em uma macro cuja definição depende #ifdef debug.

Se o relacionamento da subclasse é realmente o que você quer saber, você pode afirmar no construtor que T instanceof list(exceto que "está escrito" de maneira diferente em C ++). Dessa forma, você pode testar a saída do compilador, sem poder verificar isso por você.

Jonas Kölker
fonte
1

Não existe uma palavra-chave para esse tipo de verificação, mas você pode inserir um código que falhe pelo menos de maneira ordenada:

(1) Se você deseja que um modelo de função aceite apenas parâmetros de uma determinada classe base X, atribua-o a uma referência X em sua função. (2) Se você deseja aceitar funções, mas não primitivas ou vice-versa, ou deseja filtrar classes de outras maneiras, chame uma função auxiliar de modelo (vazia) dentro da sua função, definida apenas para as classes que deseja aceitar.

Você pode usar (1) e (2) também nas funções de membro de uma classe para forçar essas verificações de tipo em toda a classe.

Você provavelmente pode colocá-lo em alguma macro inteligente para aliviar sua dor. :)

Jaap
fonte
-2

Bem, você pode criar seu modelo lendo algo assim:

template<typename T>
class ObservableList {
  std::list<T> contained_data;
};

No entanto, isso torna a restrição implícita, e você não pode fornecer apenas algo que se pareça com uma lista. Existem outras maneiras de restringir os tipos de contêineres usados, por exemplo, usando tipos de iteradores específicos que não existem em todos os contêineres, mas novamente isso é mais implícito do que explícito.

Que eu saiba, não existe no padrão atual uma construção que espelhe a instrução Java em toda a extensão.

Existem maneiras de restringir os tipos que você pode usar dentro de um modelo que você escreve usando typedefs específicos dentro do seu modelo. Isso garantirá que a compilação da especialização de modelo para um tipo que não inclua esse typedef específico falhará, para que você possa apoiar / não selecionar seletivamente certos tipos.

No C ++ 11, a introdução de conceitos deve facilitar isso, mas não acho que ele faça exatamente o que você deseja.

Timo Geusch
fonte