Modelli C ++ che accettano solo determinati tipi


159

In Java puoi definire una classe generica che accetta solo tipi che estendono la classe di tua scelta, ad esempio:

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

Questo viene fatto usando la parola chiave "extends".

Esiste un semplice equivalente a questa parola chiave in C ++?


domanda abbastanza vecchia già ... Sento che ciò che manca qui (anche dalle risposte) è che i generici Java non sono realmente un equivalente di modelli in C ++. Ci sono somiglianze, ma dovrei stare attento a tradurre direttamente una soluzione java in C ++ solo per renderci conto che forse sono fatti per diversi tipi di problemi;)
idclev 463035818

Risposte:


104

Suggerisco di utilizzare la funzione di asserzione statica di Boost in concerto con is_base_ofdalla libreria Traits Type Boost:

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

In alcuni altri casi più semplici, puoi semplicemente dichiarare in avanti un modello globale, ma definirlo (specializzandolo in modo esplicito o parziale) solo per i tipi validi:

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.

[MODIFICA minore del 6/12/2013: l'utilizzo di un modello dichiarato ma non definito comporterà messaggi di errore del linker , non del compilatore.]


Anche le affermazioni statiche sono belle. :)
macbirdie,

5
@John: temo che la specializzazione corrisponda myBaseTypeesattamente. Prima di chiudere Boost, dovresti sapere che la maggior parte è un codice modello solo intestazione - quindi non c'è memoria o costi di tempo in fase di esecuzione per cose che non usi. Anche le cose particolari che useresti qui ( BOOST_STATIC_ASSERT()e is_base_of<>) possono essere implementate usando solo dichiarazioni (cioè nessuna definizione effettiva di funzioni o variabili) in modo che non occupino spazio o tempo.
j_random_hacker,

50
C ++ 11 è arrivato. Ora possiamo usare static_assert(std::is_base_of<List, T>::value, "T must extend list").
Siyuan Ren,

2
A proposito, il motivo per cui è necessaria la doppia parentesi è che BOOST_STATIC_ASSERT è una macro e la parentesi aggiuntiva impedisce al preprocessore di interpretare la virgola all'interno degli argomenti della funzione is_base_of come secondo argomento macro.
jfritz42,

1
@Andreyua: non capisco davvero cosa manca. Potresti provare a dichiarare una variabile my_template<int> x;o my_template<float**> y;verificare che il compilatore le consenta, quindi dichiarare una variabile my_template<char> z;e verificare che non lo faccia.
j_random_hacker,

134

Questo in genere non è giustificato in C ++, come hanno notato altre risposte qui. In C ++ tendiamo a definire tipi generici basati su altri vincoli diversi da "eredita da questa classe". Se volevi davvero farlo, è abbastanza facile farlo in 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..
};

Questo rompe molti dei concetti che la gente si aspetta in C ++. È meglio usare trucchi come definire i tuoi tratti. Ad esempio, forse observable_listvuole accettare qualsiasi tipo di contenitore che abbia i typedef const_iteratore una funzione begine endmembro che ritorni const_iterator. Se si limita questo alle classi che ereditano da listallora un utente che ha il proprio tipo da cui non eredita listma fornisce queste funzioni membro e typedefs non sarebbe in grado di usare il tuoobservable_list .

Esistono due soluzioni a questo problema, una delle quali è quella di non limitare nulla e fare affidamento sulla tipizzazione delle anatre. Un grande svantaggio di questa soluzione è che comporta un'enorme quantità di errori che possono essere difficili da individuare per gli utenti. Un'altra soluzione è quella di definire i tratti per limitare il tipo fornito per soddisfare i requisiti dell'interfaccia. Il grande svantaggio di questa soluzione è che comporta una scrittura extra che può essere vista come fastidiosa. Tuttavia, il lato positivo è che sarai in grado di scrivere i tuoi messaggi di errore alla la static_assert.

Per completezza, viene fornita la soluzione dell'esempio precedente:

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

Ci sono molti concetti mostrati nell'esempio sopra che mostrano le caratteristiche di C ++ 11. Alcuni termini di ricerca per curiosi sono modelli variadici, SFINAE, espressione SFINAE e tratti di tipo.


2
Non ho mai realizzato che i modelli C ++ usano la tipizzazione duck fino ad oggi. Un po 'bizzarro!
Andy,

2
Dati i vincoli politici estesi introdotti da C ++ in C , non so perché template<class T:list>sia un concetto così offensivo. Grazie per il consiglio.
bvj

61

La soluzione semplice, che nessuno ha ancora menzionato, è semplicemente ignorare il problema. Se provo a utilizzare un inttipo di modello in un modello di funzione che prevede una classe contenitore come vettore o elenco, visualizzerò un errore di compilazione. Grezzo e semplice, ma risolve il problema. Il compilatore tenterà di utilizzare il tipo specificato e, in caso contrario, genera un errore di compilazione.

L'unico problema è che i messaggi di errore che riceverai saranno difficili da leggere. È comunque un modo molto comune per farlo. La libreria standard è piena di modelli di funzioni o classi che prevedono determinati comportamenti dal tipo di modello e non fanno nulla per verificare che i tipi utilizzati siano validi.

Se vuoi messaggi di errore più belli (o se vuoi catturare casi che non genererebbero un errore del compilatore, ma non hanno ancora senso) puoi, a seconda di quanto tu voglia renderlo complesso, usare l'affermazione statica di Boost o la libreria Boost concept_check.

Con un compilatore aggiornato hai un built_in static_assert, che potrebbe essere usato invece.


7
Sì, ho sempre pensato che i modelli siano la cosa più simile alla duck digitando in C ++. Se ha tutti gli elementi necessari per un modello, può essere utilizzato in un modello.

@John: mi dispiace, non riesco a capirlo. Di che tipo è Te da dove viene chiamato questo codice? Senza un certo contesto, non ho alcuna possibilità di capire quel frammento di codice. Ma quello che ho detto è vero. Se si tenta di chiamare toString()un tipo che non ha una toStringfunzione membro, verrà visualizzato un errore di compilazione.
jalf

@John: la prossima volta, forse dovresti essere un po 'meno felice di innescare le persone quando il problema è nel tuo codice
jalf

@jalf, ok. +1. Questa è stata un'ottima risposta solo cercando di renderlo il migliore. Ci scusiamo per la lettura errata. Pensavo che stessimo parlando dell'utilizzo del tipo come parametro per le classi, non per i template di funzione, che suppongo siano membri del primo, ma è necessario invocare il flag per il compilatore.
Giovanni,

13

Possiamo usare std::is_base_ofe std::enable_if:
( static_assertpossono essere rimossi, le classi di cui sopra possono essere implementate su misura o utilizzate da boost se non possiamo fare riferimento 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
}

13

Per quanto ne so questo non è attualmente possibile in C ++. Tuttavia, ci sono piani per aggiungere una funzionalità chiamata "concetti" nel nuovo standard C ++ 0x che fornisce la funzionalità che stai cercando. Questo articolo di Wikipedia sui concetti di C ++ lo spiegherà in modo più dettagliato.

So che questo non risolve il tuo problema immediato ma ci sono alcuni compilatori C ++ che hanno già iniziato ad aggiungere funzionalità dal nuovo standard, quindi potrebbe essere possibile trovare un compilatore che abbia già implementato la funzionalità dei concetti.


4
Purtroppo i concetti sono stati abbandonati dallo standard.
macbirdie,

4
Vincoli e concetti dovrebbero essere adottati per C ++ 20.
Petr Javorik,

È possibile anche senza concetti, usando static_asserte SFINAE, come mostrano le altre risposte. Il problema rimanente per qualcuno proveniente da Java o C #, o Haskell (...) è che il compilatore C ++ 20 non esegue il controllo delle definizioni rispetto ai concetti richiesti, come fanno Java e C #.
user7610

10

Penso che tutte le risposte precedenti abbiano perso di vista la foresta per gli alberi.

I generici Java non sono gli stessi dei template ; usano la cancellazione del tipo , che è una tecnica dinamica , piuttosto che il polimorfismo del tempo di compilazione , che è una tecnica statica . Dovrebbe essere ovvio il motivo per cui queste due tattiche molto diverse non gelano bene.

Anziché tentare di utilizzare un costrutto di compilazione per simulare uno di runtime, diamo un'occhiata a cosa extendseffettivamente fa: secondo Stack Overflow e Wikipedia , extends viene utilizzato per indicare la sottoclasse.

C ++ supporta anche la sottoclasse.

Mostra anche una classe contenitore, che utilizza la cancellazione del tipo sotto forma di un generico, e si estende per eseguire un controllo del tipo. In C ++, devi eseguire tu stesso il tipo di macchina di cancellazione, che è semplice: fai un puntatore alla superclasse.

Inseriamolo in un typedef, per renderlo più facile da usare, piuttosto che creare un'intera classe, et voila:

typedef std::list<superclass*> subclasses_of_superclass_only_list;

Per esempio:

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

Ora, sembra che List sia un'interfaccia, che rappresenta una sorta di raccolta. Un'interfaccia in C ++ sarebbe semplicemente una classe astratta, cioè una classe che implementa solo metodi virtuali puri. Usando questo metodo, potresti facilmente implementare il tuo esempio java in C ++, senza Concetti o specializzazioni di template. Funzionerebbe anche lentamente come i generici in stile Java a causa delle ricerche nella tabella virtuale, ma questa può spesso essere una perdita accettabile.


3
Non sono un fan delle risposte che usano frasi come "dovrebbe essere ovvio" o "lo sanno tutti", e poi continuo a spiegare ciò che è ovvio o universalmente noto. Ovvio è relativo al contesto, all'esperienza e al contesto dell'esperienza. Tali dichiarazioni sono intrinsecamente maleducate.
3Daveva l'

2
@DavidLively Sono circa due anni troppo tardi per criticare questa risposta per l'etichetta, ma non sono d'accordo con te in questo caso specifico; Ho spiegato perché le due tecniche non vanno insieme prima di affermare che era ovvio, non dopo. Ho fornito il contesto e poi ho detto che la conclusione da quel contesto era ovvia. Non si adatta esattamente al tuo stampo.
Alice,

L'autore di questa risposta ha detto che qualcosa era evidente dopo aver fatto un pesante sollevamento. Non credo che l'autore intendesse dire che la soluzione era ovvia.
Luke Gehorsam,

10

Sembra un equivalente che accetta solo i tipi T derivati ​​dal tipo Elenco

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

8

Riepilogo: non farlo.

La risposta di j_random_hacker ti dice come fare. Tuttavia, vorrei anche sottolineare che si dovrebbe non fare questo. Il punto centrale dei modelli è che possono accettare qualsiasi tipo compatibile, e i vincoli del tipo di stile Java lo rompono.

I vincoli di tipo Java sono un bug, non una funzionalità. Sono lì perché Java digita la cancellazione su generici, quindi Java non riesce a capire come chiamare i metodi basati sul valore dei parametri di tipo da solo.

Il C ++ d'altra parte non ha questa limitazione. I tipi di parametri del modello possono essere di qualsiasi tipo compatibili con le operazioni con cui vengono utilizzati. Non ci deve essere una classe base comune. Questo è simile al "Duck Typing" di Python, ma fatto in fase di compilazione.

Un semplice esempio che mostra la potenza dei modelli:

// 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;
}

Questa funzione di somma può sommare un vettore di qualsiasi tipo che supporti le operazioni corrette. Funziona con entrambi i primitivi come int / long / float / double e tipi numerici definiti dall'utente che sovraccaricano l'operatore + =. Diamine, puoi anche usare questa funzione per unire le stringhe, poiché supportano + =.

Non è necessario alcun box / unboxing di primitivi.

Nota che costruisce anche nuove istanze di T usando T (). Questo è banale in C ++ usando interfacce implicite, ma non è davvero possibile in Java con vincoli di tipo.

Sebbene i modelli C ++ non abbiano vincoli di tipo espliciti, sono comunque sicuri per i tipi e non verranno compilati con codice che non supporta le operazioni corrette.


2
Se stai suggerendo di non specializzare mai i modelli, puoi anche spiegare perché è nella lingua?

1
Ottengo il tuo punto, ma se l'argomento template deve essere derivato da un tipo specifico, allora è meglio avere un messaggio facile da interpretare da static_assert rispetto al normale errore del compilatore vomit.
jhoffman0x,

1
Sì, il C ++ è più espressivo qui, ma sebbene sia generalmente una buona cosa (perché possiamo esprimere di più con meno), a volte vogliamo limitare deliberatamente il potere che ci diamo, per ottenere la certezza di comprendere appieno un sistema.
j_random_hacker,

La specializzazione di tipo @Curg è utile quando vuoi essere in grado di sfruttare alcune cose che possono essere fatte solo per determinati tipi. per esempio, un booleano è ~ normalmente ~ un byte ciascuno, anche se un byte può ~ normalmente ~ contenere 8 bit / booleani; una classe di raccolta di modelli può (e nel caso di std :: map lo fa) specializzarsi in booleano in modo che possa comprimere i dati più strettamente per risparmiare memoria.
thecoshman,

Inoltre, per chiarire, questa risposta non dice "non specializzare mai i modelli", ma non usare quella funzione per cercare di limitare quali tipi possono essere usati con un modello.
thecoshman,

6

Ciò non è possibile in C ++ semplice, ma è possibile verificare i parametri del modello in fase di compilazione tramite Concept Checking, ad esempio utilizzando il BCCL di Boost .

A partire da C ++ 20, i concetti stanno diventando una caratteristica ufficiale del linguaggio.


2
Bene, è possibile, ma il concetto di controllo è ancora una buona idea. :)
j_random_hacker,

In realtà intendevo dire che non era possibile in C ++ "semplice". ;)
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;
};

Assicurati che le classi derivate ereditino la struttura FooSecurity e che il compilatore si arrabbierà nei posti giusti.


@Zehelvion Type::FooSecurityè usato nella classe template. Se la classe, passata nell'argomento template, no FooSecurity, il tentativo di usarla provoca un errore. È sicuro che se la classe passata nell'argomento template non ha FooSecurity da cui non deriva Base.
GingerPlusPlus

2

Utilizzo del concetto C ++ 20

https://en.cppreference.com/w/cpp/language/constraints cppreference sta fornendo il caso d'uso dell'ereditarietà come esempio esplicito di concetto:

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>

Per basi multiple suppongo che la sintassi sarà:

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

GCC 10 sembra averlo implementato: https://gcc.gnu.org/gcc-10/changes.html e puoi ottenerlo come PPA su Ubuntu 20.04 . https://godbolt.org/ Il mio GCC 10.1 locale non ha conceptancora riconosciuto , quindi non sono sicuro di cosa stia succedendo.


1

Esiste un semplice equivalente a questa parola chiave in C ++?

No.

A seconda di ciò che stai cercando di realizzare, potrebbero esserci sostituti adeguati (o persino migliori).

Ho esaminato il codice STL (su Linux, penso che sia quello derivato dall'implementazione di SGI). Ha "asserzioni concettuali"; per esempio, se si richiede un tipo che capisce *xe ++x, l'asserzione del concetto conterrebbe quel codice in una funzione do-nothing (o qualcosa di simile). Richiede un certo sovraccarico, quindi potrebbe essere intelligente metterlo in una macro la cui definizione dipende#ifdef debug .

Se la relazione della sottoclasse è davvero ciò che vuoi sapere, potresti affermarlo nel costruttore T instanceof list(tranne che è "scritto" in modo diverso in C ++). In questo modo, puoi provare a uscire dal compilatore non potendo controllarlo per te.


1

Non esiste una parola chiave per tali controlli del tipo, ma è possibile inserire un codice che almeno fallirà in modo ordinato:

(1) Se si desidera che un modello di funzione accetti solo i parametri di una determinata classe base X, assegnarlo a un riferimento X nella funzione. (2) Se si desidera accettare funzioni ma non primitive o viceversa, o se si desidera filtrare le classi in altri modi, chiamare una funzione di supporto modello (vuota) all'interno della propria funzione definita solo per le classi che si desidera accettare.

È possibile utilizzare (1) e (2) anche nelle funzioni membro di una classe per forzare questi controlli del tipo sull'intera classe.

Probabilmente puoi metterlo in qualche macro intelligente per alleviare il tuo dolore. :)


-2

Bene, potresti creare il tuo modello leggendo qualcosa del genere:

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

Ciò renderà comunque implicita la restrizione, inoltre non puoi semplicemente fornire qualsiasi cosa che assomigli a un elenco. Esistono altri modi per limitare i tipi di contenitore utilizzati, ad esempio facendo uso di tipi di iteratori specifici che non esistono in tutti i contenitori, ma questo è più implicito che una restrizione esplicita.

Per quanto ne so, nella norma attuale non esiste un costrutto che rispecchi in pieno l'istruzione Java statement.

Esistono modi per limitare i tipi che è possibile utilizzare all'interno di un modello che si scrive utilizzando typedef specifici all'interno del modello. Questo assicurerà che la compilazione della specializzazione del modello per un tipo che non includa quel particolare typedef fallirà, quindi puoi supportare selettivamente / non supportare determinati tipi.

In C ++ 11, l'introduzione di concetti dovrebbe rendere tutto più semplice, ma non credo che farà esattamente quello che vorresti.

Utilizzando il nostro sito, riconosci di aver letto e compreso le nostre Informativa sui cookie e Informativa sulla privacy.
Licensed under cc by-sa 3.0 with attribution required.