web-dev-qa-db-fra.com

Fonction variadique Typesafe

Je veux écrire une fonction qui accepte un nombre variable de littéraux de chaîne. Si j'écrivais en C, je devrais écrire quelque chose comme:

void foo(const char *first, ...);

et alors l'appel ressemblerait à ceci:

foo( "hello", "world", (const char*)NULL );

On sent qu'il devrait être possible de faire mieux en C++. Le meilleur que j'ai trouvé est:

template <typename... Args>
void foo(const char* first, Args... args) {
    foo(first);
    foo(args);
}

void foo(const char* first) { /* Do actual work */ }

Appelé comme:

foo("hello", "world");

Mais je crains que la nature récursive, et le fait que nous ne fassions aucune vérification de type jusqu'à ce que nous ayons un seul argument, va créer des erreurs confuses si quelqu'un appelle foo("bad", "argument", "next", 42). Ce que je veux écrire, est quelque chose comme:

void foo(const char* args...) {
    for (const char* arg : args) {
        // Real work
    }
}

Aucune suggestion?

Edit: Il y a aussi l'option void fn(std::initializer_list<const char *> args), mais cela rend l'appel être foo({"hello", "world"});, ce que je veux éviter.

16
Martin Bonner

Bien que toutes les autres réponses résolvent le problème, vous pouvez également procéder comme suit:

namespace detail
{
    void foo(std::initializer_list<const char*> strings);
}

template<typename... Types>
void foo(const Types... strings)
{
    detail::foo({strings...});
}

Cette approche me semble (au moins) plus lisible que SFINAE et fonctionne avec C++ 11. De plus, cela vous permet de déplacer l'implémentation de foo dans un fichier cpp, ce qui pourrait également s'avérer utile.

Edit: au moins avec GCC 8.1, mon approche semble produire un meilleur message d'erreur lorsqu'elle est appelée avec des arguments non const char*:

foo("a", "b", 42, "c");

Cette implémentation compile avec:

test.cpp: In instantiation of ‘void foo_1(const ArgTypes ...) [with ArgTypes = {const char*, int, const char*, const char*}]’:
test.cpp:17:29:   required from here
test.cpp:12:16: error: invalid conversion from ‘int’ to ‘const char*’ [-fpermissive]
 detail::foo({strings...});
 ~~~~~~~~~~~^~~~~~~~~~~~~~

Tandis que SFINAE (implémentation de liliscent) produit:

test2.cpp: In function ‘int main()’:
test2.cpp:14:29: error: no matching function for call to ‘foo(const char [6], const char [6], int)’
     foo("hello", "world", 42);
                         ^
test2.cpp:7:6: note: candidate: ‘template<class ... Args, typename std::enable_if<(is_same_v<const char*, Args> && ...), int>::type <anonymous> > void foo(Args ...)’
 void foo(Args... args ){
  ^~~
test2.cpp:7:6: note:   template argument deduction/substitution failed:
test2.cpp:6:73: error: no type named ‘type’ in ‘struct std::enable_if<false, int>’
     std::enable_if_t<(std::is_same_v<const char*, Args> && ...), int> = 0>
3
joe_chip

Je pense que vous voulez probablement quelque chose comme ça:

template<class... Args,
    std::enable_if_t<(std::is_same_v<const char*, Args> && ...), int> = 0>
void foo(Args... args ){
    for (const char* arg : {args...}) {
        std::cout << arg << "\n";
    }
}

int main() {
    foo("hello", "world");
}
15
llllllllll

Remarque: il n'est pas possible de faire correspondre uniquement les littéraux de chaîne. Le plus proche que vous pouvez venir est de faire correspondre un tableau const char.

Pour effectuer la vérification de type, utilisez un modèle de fonction qui prend des tableaux const char.

Pour les parcourir en boucle avec la variable for basée sur la plage, nous devons la convertir en un initializer_list<const char*>. Nous pouvons le faire directement avec des accolades dans l'instruction for basée sur l'intervalle, car les tableaux vont se décomposer en indicateurs.

Voici à quoi ressemble le modèle de fonction (note: cela fonctionne avec zéro ou plusieurs chaînes. Pour les modifier, modifiez la signature de la fonction pour qu’elle prenne au moins un paramètre.):

template<size_t N>
using cstring_literal_type = const char (&)[N];

template<size_t... Ns>
void foo(cstring_literal_type<Ns>... args)
{
    for (const char* arg : {args...})
    {
        // Real work
    }
}
8
Nevin

+1 pour la solution l ++ de C++ 17.

Pour une solution C++ 11, vous pouvez créer un caractère de type pour créer un "et" de plusieurs valeurs (similaire à std::conjunction qui, malheureusement, n'est disponible qu'à partir de C++ 17 ... lorsque vous pouvez utiliser plier et vous n'avez plus besoin de std::conjunction (merci liliscent)).

template <bool ... Bs>
struct multAnd;

template <>
struct multAnd<> : public std::true_type
 { };

template <bool ... Bs>
struct multAnd<true, Bs...> : public multAnd<Bs...>
 { };

template <bool ... Bs>
struct multAnd<false, Bs...> : public std::false_type
 { };

alors foo() peut être écrit comme

template <typename ... Args>
typename std::enable_if<
      multAnd<std::is_same<char const *, Args>::value ...>::value>::type
   foo (Args ... args )
 {
    for (const char* arg : {args...}) {
        std::cout << arg << "\n";
    }
 }

En utilisant C++ 14, multAnd() peut être écrit en tant que fonction constexpr

template <bool ... Bs>
constexpr bool multAnd ()
 {
   using unused = bool[];

   bool ret { true };

   (void)unused { true, ret &= Bs ... };

   return ret;
 }

alors foo() devenir

template <typename ... Args>
std::enable_if_t<multAnd<std::is_same<char const *, Args>::value ...>()>
   foo (Args ... args )
 {
    for (const char* arg : {args...}) {
        std::cout << arg << "\n";
    }
 }

--- MODIFIER ---

Jarod42 (merci!) Suggère un moyen bien meilleur de développer un multAnd; quelque chose comme

template <typename T, T ...>
struct int_sequence
 { };

template <bool ... Bs>
struct all_of : public std::is_same<int_sequence<bool, true, Bs...>,
                                    int_sequence<bool, Bs..., true>>
 { };

À partir de C++ 14, vous pouvez utiliser std::integer_sequence au lieu de son imitation (int_sequence).

2
max66

À l'aide de C++ 17 expressions de pli sur l'opérateur de virgule, vous pouvez simplement effectuer les opérations suivantes:

#include <iostream>
#include <string>
#include <utility>

template<typename OneType>
void foo_(OneType&& one)
{
    std::cout << one;
}

template<typename... ArgTypes>
void foo(ArgTypes&&... arguments)
{
    (foo_(std::forward<ArgTypes>(arguments)), ...);
}

int main()
{
    foo(42, 43., "Hello", std::string("Bla"));
}

Démo en direct ici . Notez que j'ai utilisé foo_ à l'intérieur du modèle, car je ne pouvais pas être dérangé pour écrire 4 surcharges.


Si vous voulez vraiment limiter cela aux chaînes de caractères, changez la signature de la fonction comme le suggère la réponse de Nevin:

#include <cstddef>
#include <iostream>
#include <string>
#include <utility>

template<std::size_t N>
using string_literal = const char(&)[N];

template<std::size_t N>
void foo(string_literal<N> literal)
{
    std::cout << literal;
}

template<std::size_t... Ns>
void foo(string_literal<Ns>... arguments)
{
    (foo(arguments), ...);
}

int main()
{
    foo("Hello", "Bla", "haha");
}

Démo en direct ici .

Notez que ceci est extrêmement proche de la syntaxe C++ 11 pour obtenir exactement la même chose. Voir par exemple cette question de moi .

1
rubenvb

Bien, le plus proche que vous pouvez obtenir à une fonction acceptant n'importe quel nombre arbitraire de const char* mais rien d'autre n'utilise une fonction template et un transfert

void foo_impl(std::initializer_list<const char*> args)
{
    ...
}

template <class... ARGS>
auto foo(ARGS&&... args)
-> foo_impl({std::forward<ARGS>(args)...})
{
    foo_impl({std::forward<ARGS>(args)...});
}

La subtilité consiste à autoriser les conversions implicites normales.

1
Deduplicator

Et maintenant pour quelque chose de complètement différent...

Vous pouvez écrire une structure wrapper de type comme suit

template <typename, typename T>
struct wrp
 { using type = T; };

template <typename U, typename T>
using wrp_t = typename wrp<U, T>::type;

et une fonction foo() recevant une liste variée de char const * devient

template <typename ... Args>
void foo (wrp_t<Args, char const *> ... args)
 {
   for ( char const * arg : {args...} )
      std::cout << "- " << arg << std::endl;
 }

Le problème est que vous ne pouvez pas l'appeler comme vous le souhaitez

foo("hello", "world");

parce que le compilateur ne peut pas déduire les types Args....

Évidemment, vous pouvez expliquer une liste de types factices

 foo<void, void>("hello", "world");

mais je comprends que c'est une solution horrible.

Quoi qu’il en soit, si vous acceptez de passer par une fonction de modèle triviale

template <typename ... Args>
void bar (Args ... args)
 { foo<Args...>(args...); }

tu peux appeler

bar("hello", "world");

Ce qui suit est un exemple de travail complet C++ 11

#include <iostream>

template <typename, typename T>
struct wrp
 { using type = T; };

template <typename U, typename T>
using wrp_t = typename wrp<U, T>::type;

template <typename ... Args>
void foo (wrp_t<Args, char const *> ... args)
 {
   for ( char const * arg : {args...} )
      std::cout << "- " << arg << std::endl;
 }

template <typename ... Args>
void bar (Args ... args)
 { foo<Args...>(args...); }

int main ()
 {
   bar("hello", "world"); // compile

   // bar("hello", "world", 0);  // compilation error
 }
0
max66
#include<type_traits>
#include<iostream>

auto function = [](auto... cstrings) {
    static_assert((std::is_same_v<decltype(cstrings), const char*> && ...));
    for (const char* string: {cstrings...}) {
        std::cout << string << std::endl;
    }
};

int main(){    
    const char b[]= "b2";
    const char* d = "d4";
    function("a1", b, "c3", d);
    //function(a, "b", "c",42); // ERROR
}
0
NoSenseEtAl