[C++] È possibile attivare un errore di compilatore / linker se un modello non è stato istanziato con un determinato tipo?


Answers

Il compilatore instilla una funzione modello ogni volta che viene fatto riferimento e una specifica completa del modello è disponibile. Se nessuno è disponibile, il compilatore non lo fa e spera che alcune altre unità di traduzione lo istanziano. Lo stesso vale per, per esempio, il costruttore predefinito della tua classe base.

File header.h:

template<class T>
class Base
{
public:
   Base();
};

#ifndef OMIT_CONSTR
template<class T>
Base<T>::Base() { }
#endif

File client.cc:

#include "header.h"

class MyClass : public Base<int>
{
};


int main()
{
   MyClass a;
   Base<double> b;
}

File check.cc:

#define OMIT_CONSTR
#include "header.h"

void checks()
{
   Base<int> a;
   Base<float> b;
}

Poi:

 $ g++ client.cc check.cc
/tmp/cc4X95rY.o: In function `checks()':
check.cc:(.text+0x1c): undefined reference to `Base<float>::Base()'
collect2: ld returned 1 exit status

EDIT: (cercando di applicare questo all'esempio concreto)

Chiamerò questo file "loader.h":

template<class Resource>
struct loader{
  typedef Resource res_type;
  virtual res_type load(std::string const& path) const = 0;
  virtual void unload(res_type const& res) const = 0;

  loader();
};

template<class Resource>
class check_loader_instantiated_with : public loader<Resource> {
  virtual Resource load(std::string const& path) const { throw 42; }
  virtual void unload(Resource const& res) const { }
};

template<class Resource>
Resource load(std::string const& path){
  // error should be triggered here
  check_loader_instantiated_with<Resource> checker;
  // ...
}

E un altro file, "loader_impl.h":

#include "loader.h"
template<class Resource>
loader<Resource>::loader() { }

Questa soluzione ha un punto debole che io conosco. Ogni unità di compilazione ha la possibilità di includere solo loader.h o loader_impl.h. È possibile definire caricatori solo in unità di compilazione che includono loader_impl e in tali unità di compilazione il controllo degli errori è disabilitato per tutti i caricatori.

Question

Domanda di follow-up a [Il casting su un puntatore a un modello crea un'istanza di quel modello?] .

La domanda è proprio come dice il titolo, con il resto della domanda che riguarda i vincoli e gli esempi di utilizzo del modello di classe, così come i miei tentativi di raggiungere l'obiettivo.

Un vincolo importante: l'utente crea un'istanza del modello sottoclassando il mio modello di classe (e non attraverso un'istanza esplicita come nei miei tentativi di seguito). In quanto tale, è importante per me che, se possibile, l'utente non debba fare alcun lavoro extra. Solo la sottoclasse e dovrebbe funzionare (la sottoclasse in realtà si registra in un dizionario già senza che l'utente faccia qualcosa di diverso dalla sottoclassi di un modello di classe aggiuntivo con CRTP e la sottoclasse non sia mai utilizzata direttamente dall'utente che l'ha creata). Sono disposto ad accettare risposte in cui l'utente deve fare un lavoro extra (come derivare da una base aggiuntiva), se non c'è davvero altro modo.

Un frammento di codice per spiegare come verrà utilizzato il modello di classe:

// the class template in question
template<class Resource>
struct loader
{
  typedef Resource res_type;
  virtual res_type load(std::string const& path) const = 0;
  virtual void unload(res_type const& res) const = 0;
};

template<class Resource, class Derived>
struct implement_loader
  : loader<Resource>
  , auto_register_in_dict<Derived>
{
};

template<class Resource>
Resource load(std::string const& path){
  // error should be triggered here
  check_loader_instantiated_with<Resource>();

  // search through resource cache
  // ...

  // if not yet loaded, load from disk
  // loader_dict is a mapping from strings (the file extension) to loader pointers
  auto loader_dict = get_all_loaders_for<Resource>();
  auto loader_it = loader_dict.find(get_extension(path))
  if(loader_it != loader_dict.end())
    return (*loader_it)->load(path);
  // if not found, throw some exception saying that
  // no loader for that specific file extension was found
}

// the above code comes from my library, the code below is from the user

struct some_loader
  : the_lib::implement_loader<my_fancy_struct, some_loader>
{
  // to be called during registration of the loader
  static std::string extension(){ return "mfs"; }
  // override the functions and load the resource
};

E ora in forma tabellare:

  • L'utente chiama the_lib::load<my_fancy_struct> con un percorso di risorsa
  • All'interno di the_lib::load<my_fancy_struct> , se la risorsa identificata dal percorso non è già memorizzata nella cache, la carico dal disco
  • Il loader specifico da utilizzare in questo caso viene creato all'avvio e salvato in un dizionario
  • C'è un dizionario per ogni tipo di risorsa e mappano [estensione file -> puntatore loader ]
  • Se il dizionario è vuoto, anche l'utente
    • non ha creato un caricatore per quell'estensione specifica o
    • non ha creato un caricatore per quella risorsa specifica
  • Voglio solo il primo caso per farmi lanciare un'eccezione di runtime
  • Il secondo caso dovrebbe essere rilevato al momento della compilazione / collegamento, poiché coinvolge i modelli

Fondamento logico: sono fortemente a favore di errori precoci e, se possibile, voglio rilevare quanti più errori possibili prima del runtime, cioè al momento della compilazione e del collegamento. Dal momento che verificare se un caricatore per quella risorsa esiste solo coinvolgere i modelli, spero che sia possibile farlo.

L'obiettivo nei miei tentativi: attiva un errore del linker nella chiamata a check_error<char> .

// invoke with -std=c++0x on Clang and GCC, MSVC10+ already does this implicitly
#include <type_traits>

// the second parameter is for overload resolution in the first test
// literal '0' converts to as well to 'void*' as to 'foo<T>*'
// but it converts better to 'int' than to 'long'
template<class T>
void check_error(void*, long = 0);

template<class T>
struct foo{
  template<class U>
  friend typename std::enable_if<
    std::is_same<T,U>::value
  >::type check_error(foo<T>*, int = 0){}
};

template struct foo<int>;

void test();

int main(){ test(); }

Dato il codice di cui sopra, la seguente definizione di test raggiunge l'obiettivo per MSVC, GCC 4.4.5 e GCC 4.5.1 :

void test(){
  check_error<int>(0, 0); // no linker error
  check_error<char>(0, 0); // linker error for this call
}

Tuttavia, non dovrebbe farlo, poiché il passaggio di un puntatore nullo non attiva l'ADL. Perché è necessario ADL? Perché lo standard dice così:

§7.3.1.2 [namespace.memdef] p3

[...] Se una dichiarazione di friend in una classe non locale prima dichiara una classe o una funzione, la classe o la funzione friend è un membro dello spazio dei nomi che racchiude più interno. Il nome dell'amico non viene trovato dalla ricerca non qualificata o dalla ricerca qualificata finché non viene fornita una dichiarazione di corrispondenza in quell'ambito dello spazio dei nomi (prima o dopo la definizione della classe che garantisce l'amicizia). [...]

Attivare l'ADL attraverso un cast, come nella seguente definizione di test , raggiunge l'obiettivo su Clang 3.1 e GCC 4.4.5, ma GCC 4.5.1 già collega bene , così come MSVC10:

void test(){
  check_error<int>((foo<int>*)0);
  check_error<char>((foo<char>*)0);
}

Purtroppo, GCC 4.5.1 e MSVC10 hanno il comportamento corretto qui, come discusso nella domanda collegata e in particolare questa risposta .