Recherche…
Introduction
Les classes, les fonctions et les variables (depuis C ++ 14) peuvent être modélisées. Un template est un morceau de code avec des paramètres libres qui deviendront une classe concrète, une fonction ou une variable lorsque tous les paramètres sont spécifiés. Les paramètres peuvent être des types, des valeurs ou eux-mêmes des modèles. Un modèle bien connu est std::vector
, qui devient un type de conteneur concret lorsque le type d'élément est spécifié, par exemple std::vector<int>
.
Syntaxe
- modèle < template-parameter-list > déclaration
- exporter le modèle < template-parameter-list > declaration / * jusqu'à C ++ 11 * /
- modèle <> déclaration
- déclaration de modèle
- déclaration de modèle extern / * depuis C ++ 11 * /
- template < template-liste-paramètre > classe ... ( opt ) identifiant ( opt )
- template < template-liste-paramètre > identifiant de classe ( opt ) = id-expression
- template < template-liste-paramètre > typename ... ( opt ) identifiant ( opt ) / * depuis C ++ 17 * /
- template < template-liste-paramètre > identifiant typename ( opt ) = id-expression / * depuis C ++ 17 * /
- expression postfixe . expression- modèle
- Postfix expression -> id-expression de modèle
- imbriqué-name-spécificateur
template
template-simple-id::
Remarques
Le mot template
est un mot clé avec cinq significations différentes dans le langage C ++, selon le contexte.
Lorsqu'elle est suivie d'une liste de paramètres de modèle inclus dans
<>
, elle déclare un modèle tel qu'un modèle de classe , un modèle de fonction ou une spécialisation partielle d'un modèle existant.template <class T> void increment(T& x) { ++x; }
Lorsqu'il est suivi d'un vide
<>
, il déclare une spécialisation explicite (complète) .template <class T> void print(T x); template <> // <-- keyword used in this sense here void print(const char* s) { // output the content of the string printf("%s\n", s); }
Lorsqu'elle est suivie d'une déclaration sans
<>
, elle forme une déclaration ou une définition d' instanciation explicite .template <class T> std::set<T> make_singleton(T x) { return std::set<T>(x); } template std::set<int> make_singleton(int x); // <-- keyword used in this sense here
Dans une liste de paramètres de modèle, il introduit un paramètre de modèle de modèle .
template <class T, template <class U> class Alloc> // ^^^^^^^^ keyword used in this sense here class List { struct Node { T value; Node* next; }; Alloc<Node> allocator; Node* allocate_node() { return allocator.allocate(sizeof(T)); } // ... };
Après l'opérateur de résolution de portée
::
et les opérateurs d'accès de membre de classe.
et->
, il spécifie que le nom suivant est un modèle.struct Allocator { template <class T> T* allocate(); }; template <class T, class Alloc> class List { struct Node { T value; Node* next; } Alloc allocator; Node* allocate_node() { // return allocator.allocate<Node>(); // error: < and > are interpreted as // comparison operators return allocator.template allocate<Node>(); // ok; allocate is a template // ^^^^^^^^ keyword used in this sense here } };
Avant C ++ 11, un modèle pouvait être déclaré avec le mot - clé export
, ce qui en faisait un modèle exporté . La définition d'un modèle exporté n'a pas besoin d'être présente dans chaque unité de traduction dans laquelle le modèle est instancié. Par exemple, ce qui suit devait fonctionner:
foo.h
:
#ifndef FOO_H
#define FOO_H
export template <class T> T identity(T x);
#endif
foo.cpp
:
#include "foo.h"
template <class T> T identity(T x) { return x; }
main.cpp
:
#include "foo.h"
int main() {
const int x = identity(42); // x is 42
}
En raison de la difficulté de l'implémentation, le mot clé d' export
n'était pas pris en charge par la plupart des compilateurs principaux. Il a été supprimé en C ++ 11; maintenant, il est illégal d'utiliser le mot clé d' export
. Au lieu de cela, il est généralement nécessaire de définir des modèles dans les en-têtes (contrairement aux fonctions non-modèles, qui ne sont généralement pas définies dans les en-têtes). Voir Pourquoi les modèles ne peuvent-ils être implémentés que dans le fichier d'en-tête?
Modèles de fonction
Le template peut également être appliqué aux fonctions (ainsi qu'aux structures plus traditionnelles) avec le même effet.
// 'T' stands for the unknown type
// Both of our arguments will be of the same type.
template<typename T>
void printSum(T add1, T add2)
{
std::cout << (add1 + add2) << std::endl;
}
Cela peut ensuite être utilisé de la même manière que les modèles de structure.
printSum<int>(4, 5);
printSum<float>(4.5f, 8.9f);
Dans ces deux cas, l’argument template est utilisé pour remplacer les types de paramètres; le résultat fonctionne exactement comme une fonction C ++ normale (si les paramètres ne correspondent pas au type de modèle, le compilateur applique les conversions standard).
Une propriété supplémentaire des fonctions de modèle (contrairement aux classes de modèle) est que le compilateur peut déduire les paramètres du modèle en fonction des paramètres transmis à la fonction.
printSum(4, 5); // Both parameters are int.
// This allows the compiler deduce that the type
// T is also int.
printSum(5.0, 4); // In this case the parameters are two different types.
// The compiler is unable to deduce the type of T
// because there are contradictions. As a result
// this is a compile time error.
Cette fonctionnalité nous permet de simplifier le code lorsque nous combinons des structures et des fonctions de modèle. Il existe un modèle commun dans la bibliothèque standard qui nous permet de créer une template structure X
utilisant une fonction d’aide make_X()
.
// The make_X pattern looks like this.
// 1) A template structure with 1 or more template types.
template<typename T1, typename T2>
struct MyPair
{
T1 first;
T2 second;
};
// 2) A make function that has a parameter type for
// each template parameter in the template structure.
template<typename T1, typename T2>
MyPair<T1, T2> make_MyPair(T1 t1, T2 t2)
{
return MyPair<T1, T2>{t1, t2};
}
Comment ça aide?
auto val1 = MyPair<int, float>{5, 8.7}; // Create object explicitly defining the types
auto val2 = make_MyPair(5, 8.7); // Create object using the types of the paramters.
// In this code both val1 and val2 are the same
// type.
Note: Ceci n'est pas conçu pour raccourcir le code. Ceci est conçu pour rendre le code plus robuste. Il permet de modifier les types en modifiant le code dans un seul endroit plutôt que dans plusieurs emplacements.
Transmission d'argument
Le modèle peut accepter les références lvalue et rvalue en utilisant la référence de transfert :
template <typename T>
void f(T &&t);
Dans ce cas, le type réel de t
sera déduit en fonction du contexte:
struct X { };
X x;
f(x); // calls f<X&>(x)
f(X()); // calls f<X>(x)
Dans le premier cas, le type T
est déduit comme référence à X
( X&
) et le type de t
est la référence de lvalue à X
, tandis que dans le second cas, le type de T
est déduit comme X
et le type de t
comme référence de valeur à X
( X&&
).
Remarque: il convient de noter que dans le premier cas, decltype(t)
est identique à T
, mais pas dans le second.
Pour transmettre parfaitement t
à une autre fonction, que ce soit une référence lvalue ou rvalue, il faut utiliser std::forward
:
template <typename T>
void f(T &&t) {
g(std::forward<T>(t));
}
Les références de transfert peuvent être utilisées avec des modèles variadiques:
template <typename... Args>
void f(Args&&... args) {
g(std::forward<Args>(args)...);
}
Remarque: Les références de transfert ne peuvent être utilisées que pour les paramètres de modèle, par exemple, dans le code suivant, v
est une référence de valeur, pas une référence de transfert:
#include <vector>
template <typename T>
void f(std::vector<T> &&v);
Modèle de classe de base
L'idée de base d'un modèle de classe est que le paramètre template est remplacé par un type au moment de la compilation. Le résultat est que la même classe peut être réutilisée pour plusieurs types. L'utilisateur spécifie quel type sera utilisé lorsqu'une variable de la classe est déclarée. Trois exemples en sont illustrés dans main()
:
#include <iostream>
using std::cout;
template <typename T> // A simple class to hold one number of any type
class Number {
public:
void setNum(T n); // Sets the class field to the given number
T plus1() const; // returns class field's "follower"
private:
T num; // Class field
};
template <typename T> // Set the class field to the given number
void Number<T>::setNum(T n) {
num = n;
}
template <typename T> // returns class field's "follower"
T Number<T>::plus1() const {
return num + 1;
}
int main() {
Number<int> anInt; // Test with an integer (int replaces T in the class)
anInt.setNum(1);
cout << "My integer + 1 is " << anInt.plus1() << "\n"; // Prints 2
Number<double> aDouble; // Test with a double
aDouble.setNum(3.1415926535897);
cout << "My double + 1 is " << aDouble.plus1() << "\n"; // Prints 4.14159
Number<float> aFloat; // Test with a float
aFloat.setNum(1.4);
cout << "My float + 1 is " << aFloat.plus1() << "\n"; // Prints 2.4
return 0; // Successful completion
}
Spécialisation de template
Vous pouvez définir une implémentation pour des instanciations spécifiques d'une classe / méthode de modèle.
Par exemple si vous avez:
template <typename T>
T sqrt(T t) { /* Some generic implementation */ }
Vous pouvez alors écrire:
template<>
int sqrt<int>(int i) { /* Highly optimized integer implementation */ }
Ensuite, un utilisateur qui écrit sqrt(4.0)
recevra l'implémentation générique alors que sqrt(4)
obtiendra l'implémentation spécialisée.
Spécialisation du modèle partiel
Au contraire d'un modèle complet, la spécialisation du modèle partiel permet d'introduire un modèle avec certains des arguments du modèle existant. La spécialisation partielle des modèles est uniquement disponible pour les classes / structures de modèles:
// Common case:
template<typename T, typename U>
struct S {
T t_val;
U u_val;
};
// Special case when the first template argument is fixed to int
template<typename V>
struct S<int, V> {
double another_value;
int foo(double arg) {// Do something}
};
Comme indiqué ci-dessus, les spécialisations de modèles partiels peuvent introduire des ensembles de données et de membres de fonction complètement différents.
Lorsqu'un modèle partiellement spécialisé est instancié, la spécialisation la plus appropriée est sélectionnée. Par exemple, définissons un modèle et deux spécialisations partielles:
template<typename T, typename U, typename V>
struct S {
static void foo() {
std::cout << "General case\n";
}
};
template<typename U, typename V>
struct S<int, U, V> {
static void foo() {
std::cout << "T = int\n";
}
};
template<typename V>
struct S<int, double, V> {
static void foo() {
std::cout << "T = int, U = double\n";
}
};
Maintenant, les appels suivants:
S<std::string, int, double>::foo();
S<int, float, std::string>::foo();
S<int, double, std::string>::foo();
imprimera
General case
T = int
T = int, U = double
Les modèles de fonction ne peuvent être que entièrement spécialisés:
template<typename T, typename U>
void foo(T t, U u) {
std::cout << "General case: " << t << " " << u << std::endl;
}
// OK.
template<>
void foo<int, int>(int a1, int a2) {
std::cout << "Two ints: " << a1 << " " << a2 << std::endl;
}
void invoke_foo() {
foo(1, 2.1); // Prints "General case: 1 2.1"
foo(1,2); // Prints "Two ints: 1 2"
}
// Compilation error: partial function specialization is not allowed.
template<typename U>
void foo<std::string, U>(std::string t, U u) {
std::cout << "General case: " << t << " " << u << std::endl;
}
Valeur du paramètre de modèle par défaut
Tout comme dans le cas des arguments de fonction, les paramètres du modèle peuvent avoir leurs valeurs par défaut. Tous les paramètres de modèle avec une valeur par défaut doivent être déclarés à la fin de la liste des paramètres du modèle. L'idée de base est que les paramètres du modèle avec la valeur par défaut peuvent être omis lors de l'instanciation du modèle.
Exemple simple d'utilisation du paramètre template default:
template <class T, size_t N = 10>
struct my_array {
T arr[N];
};
int main() {
/* Default parameter is ignored, N = 5 */
my_array<int, 5> a;
/* Print the length of a.arr: 5 */
std::cout << sizeof(a.arr) / sizeof(int) << std::endl;
/* Last parameter is omitted, N = 10 */
my_array<int> b;
/* Print the length of a.arr: 10 */
std::cout << sizeof(b.arr) / sizeof(int) << std::endl;
}
Modèle d'alias
Exemple de base:
template<typename T> using pointer = T*;
Cette définition fait du pointer<T>
un alias de T*
. Par exemple:
pointer<int> p = new int; // equivalent to: int* p = new int;
Les modèles d'alias ne peuvent pas être spécialisés. Cependant, cette fonctionnalité peut être obtenue indirectement en les faisant référence à un type imbriqué dans une structure:
template<typename T>
struct nonconst_pointer_helper { typedef T* type; };
template<typename T>
struct nonconst_pointer_helper<T const> { typedef T* type; };
template<typename T> using nonconst_pointer = nonconst_pointer_helper<T>::type;
Paramètres du modèle de modèle
Parfois, nous aimerions passer dans le modèle un type de modèle sans en fixer les valeurs. C'est pour cela que sont créés les paramètres du modèle de modèle. Exemples de paramètres de modèle de modèle très simples:
template <class T>
struct Tag1 { };
template <class T>
struct Tag2 { };
template <template <class> class Tag>
struct IntTag {
typedef Tag<int> type;
};
int main() {
IntTag<Tag1>::type t;
}
#include <vector>
#include <iostream>
template <class T, template <class...> class C, class U>
C<T> cast_all(const C<U> &c) {
C<T> result(c.begin(), c.end());
return result;
}
int main() {
std::vector<float> vf = {1.2, 2.6, 3.7};
auto vi = cast_all<int>(vf);
for(auto &&i: vi) {
std::cout << i << std::endl;
}
}
Déclaration des arguments de modèle non-type avec auto
Avant C ++ 17, lorsque vous écriviez un paramètre de type non-modèle, vous deviez d'abord spécifier son type. Donc, un modèle commun est devenu écrit quelque chose comme:
template <class T, T N>
struct integral_constant {
using type = T;
static constexpr T value = N;
};
using five = integral_constant<int, 5>;
Mais pour les expressions compliquées, utiliser quelque chose comme cela implique d'avoir à écrire decltype(expr), expr
lors de l'instanciation des modèles. La solution consiste à simplifier cet idiome et à autoriser simplement l’ auto
:
template <auto N>
struct integral_constant {
using type = decltype(N);
static constexpr type value = N;
};
using five = integral_constant<5>;
Vide deleter personnalisé pour unique_ptr
Un bon exemple de motivation peut être d’essayer de combiner l’optimisation de la base vide avec un paramètre personnalisé pour unique_ptr
. Différents déléteurs de l'API C ont des types de retour différents, mais peu importe - nous voulons simplement que quelque chose fonctionne pour n'importe quelle fonction:
template <auto DeleteFn>
struct FunctionDeleter {
template <class T>
void operator()(T* ptr) const {
DeleteFn(ptr);
}
};
template <T, auto DeleteFn>
using unique_ptr_deleter = std::unique_ptr<T, FunctionDeleter<DeleteFn>>;
Et maintenant, vous pouvez simplement utiliser n'importe quel pointeur de fonction qui peut prendre un argument de type T
tant que paramètre non-type de modèle, quel que soit le type de retour, et en extraire un en-tête unique_ptr
:
unique_ptr_deleter<std::FILE, std::fclose> p;
Paramètre de type non-type
Outre les types en tant que paramètre de modèle, nous sommes autorisés à déclarer des valeurs d'expressions constantes répondant à l'un des critères suivants:
- type intégral ou énumération,
- pointeur vers un objet ou un pointeur vers une fonction,
- référence lvalue à une référence à une fonction objet ou lvalue,
- pointeur vers membre,
-
std::nullptr_t
.
Comme tous les paramètres de modèle, les paramètres de modèle non typés peuvent être explicitement spécifiés, définis par défaut ou déduits implicitement via la déduction de l'argument de modèle.
Exemple d'utilisation du paramètre template non-type:
#include <iostream>
template<typename T, std::size_t size>
std::size_t size_of(T (&anArray)[size]) // Pass array by reference. Requires.
{ // an exact size. We allow all sizes
return size; // by using a template "size".
}
int main()
{
char anArrayOfChar[15];
std::cout << "anArrayOfChar: " << size_of(anArrayOfChar) << "\n";
int anArrayOfData[] = {1,2,3,4,5,6,7,8,9};
std::cout << "anArrayOfData: " << size_of(anArrayOfData) << "\n";
}
Exemple de spécification explicite des paramètres de type type et non-type:
#include <array>
int main ()
{
std::array<int, 5> foo; // int is a type parameter, 5 is non-type
}
Les paramètres de modèle non typés sont l'un des moyens d'obtenir une récurrence du modèle et permettent de réaliser une métaprogrammation .
Structures de données du modèle Variadic
Il est souvent utile de définir des classes ou des structures ayant un nombre et un type variables de membres de données définis au moment de la compilation. L'exemple canonique est std::tuple
, mais il est parfois nécessaire de définir vos propres structures personnalisées. Voici un exemple qui définit la structure en utilisant la composition (plutôt que l'héritage comme avec std::tuple
. Commencez par la définition générale (vide), qui sert également de base à la fin de la recrusion dans la spécialisation ultérieure:
template<typename ... T>
struct DataStructure {};
Cela nous permet déjà de définir une structure vide, DataStructure<> data
, même si cela n’est pas encore très utile.
Vient ensuite la spécialisation récursive:
template<typename T, typename ... Rest>
struct DataStructure<T, Rest ...>
{
DataStructure(const T& first, const Rest& ... rest)
: first(first)
, rest(rest...)
{}
T first;
DataStructure<Rest ... > rest;
};
Cela nous suffit maintenant pour créer des structures de données arbitraires, telles que DataStructure<int, float, std::string> data(1, 2.1, "hello")
.
Alors que se passe-t-il? Tout d'abord, notez qu'il s'agit d'une spécialisation nécessitant qu'au moins un paramètre de modèle variadic (à savoir T
ci-dessus) existe, sans se soucier de la composition spécifique du pack Rest
. Savoir que T
existe permet de définir son membre de données en first
. Le reste des données est empaqueté récursivement en tant que DataStructure<Rest ... > rest
. Le constructeur initie ces deux membres, y compris un appel de constructeur récursif au membre rest
.
Pour mieux comprendre cela, nous pouvons travailler sur un exemple: supposons que vous ayez une déclaration DataStructure<int, float> data
. La déclaration commence par correspondre à la spécialisation, produisant une structure avec les membres de données int first
et DataStructure<float> rest
. La définition de rest
correspond à nouveau à cette spécialisation, en créant ses propres membres float first
et DataStructure<> rest
. Enfin, ce dernier rest
correspond à la définition de base, produisant une structure vide.
Vous pouvez visualiser ceci comme suit:
DataStructure<int, float>
-> int first
-> DataStructure<float> rest
-> float first
-> DataStructure<> rest
-> (empty)
Maintenant nous avons la structure de données, mais ce n'est pas encore très utile car nous ne pouvons pas accéder facilement aux éléments de données individuels (par exemple, pour accéder au dernier membre des données DataStructure<int, float, std::string> data
nous devrions utiliser des data.rest.rest.first
, qui n’est pas exactement convivial, Nous ajoutons donc un get
méthode (seulement nécessaire dans la spécialisation que la structure de base cas n'a aucune donnée à get
):
template<typename T, typename ... Rest>
struct DataStructure<T, Rest ...>
{
...
template<size_t idx>
auto get()
{
return GetHelper<idx, DataStructure<T,Rest...>>::get(*this);
}
...
};
Comme vous pouvez le voir, cette fonction get
member est elle-même basée sur un modèle - cette fois-ci sur l'index du membre requis (l'utilisation peut donc être data.get<1>()
chose comme data.get<1>()
, similaire à std::tuple
). Le travail réel est effectué par une fonction statique dans une classe auxiliaire, GetHelper
. La raison pour laquelle nous ne pouvons pas définir les fonctionnalités requises directement dans DataStructure
de get
idx
DataStructure
de get
est parce que (comme nous le verrons bientôt voir) , nous aurions besoin de se spécialiser sur idx
- mais il est impossible de se spécialiser en fonction de membre de modèle sans se spécialiser la classe contenant modèle. Notez également que l'utilisation d'une auto
style C ++ 14 rend nos vies beaucoup plus simples car sinon nous aurions besoin d'une expression assez compliquée pour le type de retour.
Donc, à la classe d'assistance. Cette fois, nous aurons besoin d'une déclaration préalable vide et de deux spécialisations. D'abord la déclaration:
template<size_t idx, typename T>
struct GetHelper;
Maintenant, le cas de base (quand idx==0
). Dans ce cas, nous renvoyons simplement le first
membre:
template<typename T, typename ... Rest>
struct GetHelper<0, DataStructure<T, Rest ... >>
{
static T get(DataStructure<T, Rest...>& data)
{
return data.first;
}
};
Dans le cas récursif, nous décrémentons idx
et GetHelper
le GetHelper
pour le membre rest
:
template<size_t idx, typename T, typename ... Rest>
struct GetHelper<idx, DataStructure<T, Rest ... >>
{
static auto get(DataStructure<T, Rest...>& data)
{
return GetHelper<idx-1, DataStructure<Rest ...>>::get(data.rest);
}
};
Pour travailler sur un exemple, supposons que nous ayons DataStructure<int, float> data
et que nous avons besoin de data.get<1>()
. Cela appelle GetHelper<1, DataStructure<int, float>>::get(data)
(la 2ème spécialisation), qui à son tour appelle GetHelper<0, DataStructure<float>>::get(data.rest)
, qui retourne finalement (par la 1ère spécialisation comme maintenant idx
est 0) data.rest.first
.
Alors c'est tout! Voici le code de fonctionnement complet, avec quelques exemples d'utilisation dans la fonction main
:
#include <iostream>
template<size_t idx, typename T>
struct GetHelper;
template<typename ... T>
struct DataStructure
{
};
template<typename T, typename ... Rest>
struct DataStructure<T, Rest ...>
{
DataStructure(const T& first, const Rest& ... rest)
: first(first)
, rest(rest...)
{}
T first;
DataStructure<Rest ... > rest;
template<size_t idx>
auto get()
{
return GetHelper<idx, DataStructure<T,Rest...>>::get(*this);
}
};
template<typename T, typename ... Rest>
struct GetHelper<0, DataStructure<T, Rest ... >>
{
static T get(DataStructure<T, Rest...>& data)
{
return data.first;
}
};
template<size_t idx, typename T, typename ... Rest>
struct GetHelper<idx, DataStructure<T, Rest ... >>
{
static auto get(DataStructure<T, Rest...>& data)
{
return GetHelper<idx-1, DataStructure<Rest ...>>::get(data.rest);
}
};
int main()
{
DataStructure<int, float, std::string> data(1, 2.1, "Hello");
std::cout << data.get<0>() << std::endl;
std::cout << data.get<1>() << std::endl;
std::cout << data.get<2>() << std::endl;
return 0;
}
Instanciation explicite
Une définition d'instanciation explicite crée et déclare une classe, une fonction ou une variable concrète à partir d'un modèle, sans l'utiliser pour l'instant. Une instanciation explicite peut être référencée à partir d'autres unités de traduction. Cela peut être utilisé pour éviter de définir un modèle dans un fichier d'en-tête, s'il n'est instancié qu'avec un ensemble fini d'arguments. Par exemple:
// print_string.h
template <class T>
void print_string(const T* str);
// print_string.cpp
#include "print_string.h"
template void print_string(const char*);
template void print_string(const wchar_t*);
Comme print_string<char>
et print_string<wchar_t>
sont explicitement instanciés dans print_string.cpp
, l'éditeur de liens peut les trouver même si le modèle print_string
n'est pas défini dans l'en-tête. Si ces déclarations d'instanciation explicites n'étaient pas présentes, une erreur de l'éditeur de liens se produirait probablement. Voir Pourquoi les modèles ne peuvent-ils être implémentés que dans le fichier d'en-tête?
Si une définition d’instanciation explicite est précédée du mot clé extern
, elle devient une déclaration d’ instanciation explicite à la place. La présence d'une déclaration d'instanciation explicite pour une spécialisation donnée empêche l'instanciation implicite de la spécialisation donnée au sein de l'unité de traduction en cours. Au lieu de cela, une référence à cette spécialisation qui provoquerait une instanciation implicite peut faire référence à une définition d’instanciation explicite dans la même UT ou une autre.
foo.h
#ifndef FOO_H
#define FOO_H
template <class T> void foo(T x) {
// complicated implementation
}
#endif
foo.cpp
#include "foo.h"
// explicit instantiation definitions for common cases
template void foo(int);
template void foo(double);
main.cpp
#include "foo.h"
// we already know foo.cpp has explicit instantiation definitions for these
extern template void foo(double);
int main() {
foo(42); // instantiates foo<int> here;
// wasteful since foo.cpp provides an explicit instantiation already!
foo(3.14); // does not instantiate foo<double> here;
// uses instantiation of foo<double> in foo.cpp instead
}