Typesafe variadic function

Martin Bonner 05/15/2018. 8 answers, 458 views
c++ c++11 templates variadic-templates c++17

I want to write a function that accepts a variable number of string literals. If I was writing in C, I would have to write something like:

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

and then the call would look like:

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

It feels like it ought to be possible to do better in C++. The best I have come up with is:

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

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

Called as:

foo("hello", "world");

But I fear that the recursive nature, and the fact that we don't do any type checking until we get to a single argument, is going to make errors confusing if somebody calls foo("bad", "argument", "next", 42). What I want to write, is something like:

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

Any suggestions?

Edit: There is also the option of void fn(std::initializer_list<const char *> args), but that makes the call be foo({"hello", "world"}); which I want to avoid.

8 Answers


liliscent 05/15/2018.

I think you probably want something like this:

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

Nevin 05/15/2018.

Note: it is not possible to match just string literals. The closest you can come is to match a const char array.

To do the type checking, use a function template which takes const char arrays.

To loop over them with range-based for, we need to convert it to an initializer_list<const char*>. We can do so directly with braces in the range-based for statement, because arrays will decay to pointers.

Here is what the function template looks like (note: this works on zero or more string literals. If you want one or more, change the function signature to take at least one parameter.):

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

max66 05/15/2018.

+1 for the C++17 liliscent's solution.

For a C++11 solution, a possible way is create a type traits to make an "and" of multiple values (something similar to std::conjunction that, unfortunately, is available only starting from C++17... when you can use folding and you don't need std::conjunction anymore (thanks 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
 { };

so foo() can be written as

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

Using C++14, multAnd() can be written as a constexpr function

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

   bool ret { true };

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

   return ret;
 }

so foo() become

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

--- EDIT ---

Jarod42 (thanks!) suggest a far better way to develop a multAnd; something as

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

Starting from C++14 can be used std::integer_sequence instead of it's imitation (int_sequence).


rubenvb 05/15/2018.

Using C++17 fold expressions on the comma operator, you can simply do the following:

#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"));
}

Live demo here. Note I used foo_ inside the template, because I couldn't be bothered to write out 4 overloads.


If you really really really want to restrict this to string literals, change the function signature as Nevin's answer suggests:

#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");
}

Live demo here.

Note this is extremely close to the C++11 syntax to achieve the exact same thing. See e.g. this question of mine.


user8964493 05/15/2018.

While all other answers solve the problem, you could also do the following:

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

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

This approach seems (at least to me) to be more readable than using SFINAE and works with C++11. Moreover, it allows you to move implementation of foo to a cpp file, which might be useful too.

Edit: at least with GCC 8.1, my approach seems to produce better error message when called with non const char* arguments:

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

This implementation compiles with:

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...});
 ~~~~~~~~~~~^~~~~~~~~~~~~~

While SFINAE-based (liliscent's implementation) produces:

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>

NoSenseEtAl 05/15/2018.
#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
}

alfC 05/16/2018 at 01:07.

Of course it is possible, this compiles and runs what you want (pay attention)

#include<iostream>

template<class... Args>                                                            // hehe, here is the secret vvvvv      
auto foo(Args... args )                                                            ->decltype((char const*)(*std::begin({args...})), (char const*)(*std::end({args...})), void(0))
{
    for (const char* arg : {args...}) {
        std::cout << arg << "\n";
    }
}

int main() {
    foo("hello", "dd");
}

This is @liliscent solution but with more sugar and, to please @rubenvb, without enable_if. If you think the extra code as a comment (which is not), note that you'll see exactly the syntax you are looking for.

Note that you can only feed an homogeneous list of things that is convertible to char const*, which was one of your goals it seems.


Deduplicator 05/16/2018 at 02:23.

Well, the nearest you can get to a function accepting any arbitrary number of const char* but nothing else uses a template-function and forwarding:

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

The subtlety is in allowing the normal implicit conversions.

Related questions

Hot questions

Language

Popular Tags