Comment appeler une fonction template si elle existe, et autre chose autrement?
Je veux faire quelque chose comme
template <typename T>
void foo(const T& t) {
IF bar(t) would compile
bar(t);
ELSE
baz(t);
}
Je pensais que quelque chose en utilisant enable_if
ferait le travail ici, en divisant foo
en deux morceaux, mais je n'arrive pas à comprendre les détails. Quel est le moyen le plus simple d'y parvenir?
7 réponses
Deux recherches sont effectuées pour le nom bar
. L'un est la recherche non qualifiée dans le contexte de définition de foo
. L'autre est la recherche dépendante de l'argument à chaque contexte d'instanciation (mais le résultat de la recherche à chaque contexte d'instanciation n'est pas autorisé à changer de comportement entre deux contextes d'instanciation différents).
Pour obtenir le comportement souhaité, vous pouvez définir une fonction de secours dans un espace de noms fallback
qui renvoie un type
namespace fallback {
// sizeof > 1
struct flag { char c[2]; };
flag bar(...);
}
La fonction bar
sera appelée si rien d'autre ne correspond car les points de suspension ont le plus mauvais coût de conversion. Maintenant, incluez ces candidats dans votre fonction par une directive using de fallback
, de sorte que fallback::bar
est inclus en tant que candidat dans l'appel à bar
.
Maintenant, pour voir si l'appel de bar
résout à votre fonction, vous l'appellerez, et de vérifier si le type de retour est flag
. Le type de retour d'une fonction autrement choisie pourrait être void, donc vous devez faire quelques astuces d'opérateur de virgule pour contourner cela.
namespace fallback {
int operator,(flag, flag);
// map everything else to void
template<typename T>
void operator,(flag, T const&);
// sizeof 1
char operator,(int, flag);
}
Si notre fonction a été sélectionnée, l'appel de l'opérateur virgule retournera une référence à int
. Si ce n'est pas le cas ou si la fonction sélectionnée renvoie void
, l'appel renvoie void
à son tour. Ensuite, l'invocation suivante avec flag
comme deuxième argument retournera un type qui a sizeof 1 si notre repli a été sélectionné, et un sizeof plus grand 1 (l'opérateur virgule intégré sera utilisé car void
est dans le mix) si quelque chose d'autre a été sélectionné.
Nous comparons le sizeof et le delegate à une structure.
template<bool>
struct foo_impl;
/* bar available */
template<>
struct foo_impl<true> {
template<typename T>
static void foo(T const &t) {
bar(t);
}
};
/* bar not available */
template<>
struct foo_impl<false> {
template<typename T>
static void foo(T const&) {
std::cout << "not available, calling baz...";
}
};
template <typename T>
void foo(const T& t) {
using namespace fallback;
foo_impl<sizeof (fallback::flag(), bar(t), fallback::flag()) != 1>
::foo(t);
}
Cette solution est ambiguë si la fonction existante a aussi une ellipse. Mais cela semble être plutôt rare. Test en utilisant le repli:
struct C { };
int main() {
// => "not available, calling baz..."
foo(C());
}
Et si un candidat est trouvé en utilisant la recherche dépendante de l'argument
struct C { };
void bar(C) {
std::cout << "called!";
}
int main() {
// => "called!"
foo(C());
}
Pour tester la recherche non qualifiée au contexte de définition, définissons la fonction suivante au-dessus de foo_impl
et foo
(Placez le modèle foo_impl au-dessus de foo
, de sorte qu'ils ont les deux le même contexte de définition)
void bar(double d) {
std::cout << "bar(double) called!";
}
// ... foo template ...
int main() {
// => "bar(double) called!"
foo(12);
}
Litb vous a donné une très bonne réponse. Cependant, je me demande si, étant donné plus de contexte, nous ne pourrions pas trouver quelque chose de moins générique, mais aussi moins, euh, élaboré ?
Par exemple, quels types peuvent être T
? Quoi que ce soit? Quelques types? Un ensemble très restreint sur lequel vous avez le contrôle? Certaines classes que vous concevez en conjonction avec la fonction foo
? Compte tenu de ce dernier, vous pouvez simplement mettre quelque chose comme
typedef boolean<true> has_bar_func;
Dans les types, puis passez à différentes surcharges foo
basées sur cela:
template <typename T>
void foo_impl(const T& t, boolean<true> /*has_bar_func*/);
template <typename T>
void foo_impl(const T& t, boolean<false> /*has_bar_func*/);
template <typename T>
void foo(const T& t) {
foo_impl( t, typename T::has_bar_func() );
}
Aussi, peut le bar
/baz
la fonction a à peu près n'importe quelle signature, y a-t-il un ensemble quelque peu restreint, ou y a-t-il juste une signature valide? Si ce dernier, l'idée de repli (excellente) de litb, en conjonction avec une méta-fonction utilisant sizeof
pourrait être un peu plus simple. Mais ce que je n'ai pas exploré, donc c'est juste une pensée.
Je pense que la solution de litb fonctionne, mais est trop complexe. La raison en est qu'il introduit une fonction fallback::bar(...)
qui agit comme une "fonction de dernier recours", puis s'efforce de ne pas l'appeler. Pourquoi? Il semble que nous ayons un comportement parfait pour cela:
namespace fallback {
template<typename T>
inline void bar(T const& t, ...)
{
baz(t);
}
}
template<typename T>
void foo(T const& t)
{
using namespace fallback;
bar(t);
}
Mais comme je l'ai indiqué dans un commentaire au post original de litb, il y a plusieurs raisons pour lesquelles bar(t)
pourrait échouer à compiler, et je ne suis pas certain que cette solution gère les mêmes cas. Il va certainement échouer sur un private bar::bar(T t)
Si vous êtes prêt à vous limiter à Visual C++, vous pouvez utiliser le ___si il existe et ___si non_existe consolidés.
Pratique dans un pincement, mais spécifique à la plate-forme.
EDIT: j'ai parlé trop tôt! la réponse de litb montre comment cela peut réellement être fait (au coût possible de votre santé mentale... :- P)
Malheureusement, je pense que le cas général de la vérification "cette compilation" est hors de portée de déduction de l'argument du modèle de fonction + SFINAE, qui est l'astuce habituelle pour ce genre de choses. Je pense que le mieux que vous pouvez faire est de créer un modèle de fonction "sauvegarde":
template <typename T>
void bar(T t) { // "Backup" bar() template
baz(t);
}
Puis changez foo()
pour tout simplement:
template <typename T>
void foo(const T& t) {
bar(t);
}
Cela fonctionnera dans la plupart des cas. Étant donné que le type de paramètre du modèle bar()
est T
, il sera considéré comme "moins spécialisé" par rapport à toute autre fonction ou modèle de fonction nommé bar()
et cédera donc la priorité à cette fonction ou modèle de fonction préexistant lors de la résolution de surcharge. Sauf que:
- si le
bar()
préexistant est lui-même un modèle de fonction prenant un paramètre de modèle de typeT
, une ambiguïté surgira car aucun modèle n'est plus spécialisé que l'autre, et le compilateur se plaindra. - les conversions implicites ne fonctionneront pas non plus, et conduiront à des problèmes difficiles à diagnostiquer: supposons qu'il y ait un
bar(long)
préexistant mais quefoo(123)
soit appelé. Dans ce cas, le compilateur choisira tranquillement d'instancier le modèle "backup"bar()
avecT = int
au lieu d'effectuer la promotionint->long
, Même si cette dernière aurait compilé et fonctionné correctement!
En bref: il n'y a pas facile, solution complète, et je suis sûr qu'il n'y a même pas une solution complète et délicate. :(
//default
//////////////////////////////////////////
template <class T>
void foo(const T& t){
baz(t);
}
//specializations
//////////////////////////////////////////
template <>
void foo(const specialization_1& t){
bar(t);
}
....
template <>
void foo(const specialization_n& t){
bar(t);
}