16

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.

llllllllll
  • 16,169
  • 4
  • 31
  • 54
  • 1
    Are you tied to the call syntax `foo("hello", "world");`? If not, then a `std::initializer_list` is an appropriate parameter type – Caleth May 15 '18 at 15:08
  • 3
    Doesn't this trigger the [parameter packs not expanded with '…'](https://stackoverflow.com/questions/33868486/parameter-packs-not-expanded-with)? – gsamaras May 15 '18 at 15:10
  • @gsamaras My desired syntax doesn't work at all. If `T args...` meant "`std::initializer_list` initialized from all remaining arguments, then no. – Martin Bonner supports Monica May 15 '18 at 15:22
  • @Caleth - I've updated the question: I want to avoid ({}) (but thanks for clarifying my requirements) – Martin Bonner supports Monica May 15 '18 at 15:25
  • Re: "fear that [it's] going to make errors confusing" -- try it. – Pete Becker May 15 '18 at 20:02
  • This isn't really answering the question, but the simplest way to do this might be to pass a `std::vector` rather than messing with variadic args – Hong Ooi May 16 '18 at 01:27
  • Only string-literals, or anything implicitly convertible to `const char*`? – Deduplicator May 16 '18 at 02:13
  • @Deduplicator - in my *specific* case, anything convertible to `const char *`. The only time I can see you would want to distinguish between that and a string literal, is when you want to depend on the static lifetime of the literal - but there's no way for the type system to distinguish `const char foo[] = "bad";` (with automatic lifetime) from `"good"`. – Martin Bonner supports Monica May 16 '18 at 07:13

9 Answers9

15

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");
}
llllllllll
  • 16,169
  • 4
  • 31
  • 54
  • 1
    This is quite heavy for what it achieves... Not to say complete abuse of `enable_if` vs specifying the type of the arguments. – rubenvb May 15 '18 at 17:50
  • 1
    @rubenvb How do you specify variadic number of arguments of a same type ? I don't understand what you mean by "abuse". – llllllllll May 15 '18 at 17:54
  • 1
    See my answer and also @Nevin's. – rubenvb May 15 '18 at 17:55
  • 3
    @Deduplicator Then use `std::is_convertible_v`. It's not clear whether they want implicit conversion. It's even not clear whether they want just a hard error or SFINAE. That's why my answer starts with *I think you probably want something like this* – llllllllll May 16 '18 at 05:03
8

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
    }
}
Nevin
  • 4,595
  • 18
  • 24
  • very nice... a little suggestion: add an example of your solution over the "What I want to write, is something like" code in the question. – max66 May 15 '18 at 18:36
  • @max66, I updated the code and explanation to use range-based for, as per the OP request for syntax. – Nevin May 15 '18 at 21:12
4

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>
joe_chip
  • 2,468
  • 1
  • 12
  • 23
2

+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).

max66
  • 65,235
  • 10
  • 71
  • 111
  • 1
    Upvoted. It's worth mentioning that this is actually an implementation of `std::conjunction` in C++17. – llllllllll May 15 '18 at 15:58
  • @liliscent - `std::conjunction`! I was remembering that there is something similar but the name doesn't come in my mind. Thanks. – max66 May 15 '18 at 16:17
  • 1
    `multAnd` can be implemented that way too: `template struct Bools{}; template struct all_of : std::is_same, Bools> {};` – Jarod42 May 15 '18 at 16:40
  • @Jarod42 - I don't know how I didn't think about it: the `std::is_same` check over the shifted list is one of my preferred tricks. Thanks. – max66 May 15 '18 at 17:05
  • It's somewhat unnatural to disallow all implicit conversion. – Deduplicator May 16 '18 at 02:25
1

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.

rubenvb
  • 74,642
  • 33
  • 187
  • 332
  • 1
    You just removed the `enable_if`. And your answer doesn't answer the question, the question specifically wants a `for` loop. – llllllllll May 15 '18 at 17:59
  • 1
    My code works as if a for loop was written, minus the temporary copies of the arguments in an initializer_list. – rubenvb May 15 '18 at 18:11
  • Your first version addresses heterogeneous types, which cannot be written into a `for` loop. Your second version are also heterogeneous, not to mention that OP wants `const char*`. I wouldn't say that's bad, but your criticism of my code with a downvote is going too far. – llllllllll May 15 '18 at 18:19
  • 2
    @liliscent I don't specifically want a for loop - that's just what I wrote in the example. Sorry for the lack of clarity. – Martin Bonner supports Monica May 16 '18 at 07:09
1

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.

Deduplicator
  • 44,692
  • 7
  • 66
  • 118
0
#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
}
NoSenseEtAl
  • 28,205
  • 28
  • 128
  • 277
0

And now... for something completely different...

You can write a type wrapper struct as follows

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

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

and a foo() function receiving a variadic list of char const * simply become

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

The problem is that you can't call it as you want

foo("hello", "world");

because the compiler isn't able to deduce the Args... types.

Obviously you can explicit a list of dummy types

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

but I understand that is a horrible solution.

Anyway, if you accept to pass through a trivial template function

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

you can call

bar("hello", "world");

The following is a full C++11 working example

#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
 }
max66
  • 65,235
  • 10
  • 71
  • 111
-2

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

#include<iostream>
                                                                                          template<class... Char>
                                                                                          // hehe, here is the secret      
auto foo(const Char*... 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("no", "sense","of","humor");
}

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.

alfC
  • 14,261
  • 4
  • 67
  • 118