1

I'm writing a utility function in c++ 11 that adds an element of a single type to a vector. Most variable argument docs/guides I've found show a template with the typedef type, but I'm looking to only allow a single type for all the variable arguments (const char*). The following is the relevant snippet of code:

Item.hpp:

// Guard removed
#include <vector>

class Item {
  public:
    Item(const char* n, bool (*optionChange)(uint8_t), const char*...);
  private:
    std::vector<const char*> options;
    void addOption(const char*);
}

Item.cpp:

#include "Item.hpp"

void Item::addOption(const char* option) {
  options.push_back(option);
}

Item::Item(
  const char* n, 
  bool (*optionChange)(uint8_t),
  const char* opts...
): name(n), type(MENU_TYPE_OPTS), selectedOpt(0) {
  addOption(opts...); // Doesn't compile
}

Compilation of the above code fails with the message error: expansion pattern 'opts' contains no argument packs.

CryptoAlgorithm
  • 838
  • 5
  • 15
  • 4
    There are no "strongly typed varargs", it's just not part of the language. There are however [*template parameter packs*](https://en.cppreference.com/w/cpp/language/parameter_pack) which might be used in a similar way. – Some programmer dude Aug 24 '22 at 13:21
  • I've tried parameter packs too, but somehow couldn't get it to compile when I provide a type instead of just using a generic type: `template` – CryptoAlgorithm Aug 24 '22 at 13:29
  • What is `optionChange` used for? Please remove everything that is not relevant to the problem you are trying to solve. – Ted Lyngmo Aug 24 '22 at 13:33
  • Template parameter packs, or optional function parameters. Don't use `char*` pointers are too tricky to handle, prefer `std::string` which supports most functionality like primitive types. – πάντα ῥεῖ Aug 24 '22 at 13:33
  • @πάνταῥεῖ I'm developing for an embedded platform, so I'd rather my stack stay relatively intact. `std:string` has a bad habit of causing stack fragmentation and crashes, so I'm trying to steer clear of that as far as possible. – CryptoAlgorithm Aug 24 '22 at 13:43
  • @TedLyngmo I left that in as I didn't want to change the constructor symbol too much, lest it affects the solution. – CryptoAlgorithm Aug 24 '22 at 13:45
  • @CryptoAlgorithm Well, you shouldn't leave things in that we can't figure out what to do with. Anyway, you could just initialize the `options` `vector` directly from a parameter pack: https://godbolt.org/z/fneWvnzxb – Ted Lyngmo Aug 24 '22 at 14:02
  • @CryptoAlgorithm: The question was closed before I could post my long answer, so I'll just give you the working demo: https://godbolt.org/z/n69MaqT1e – AndyG Aug 24 '22 at 14:16

2 Answers2

5

Use a variadic template. With variadic template, also all types can be different, but you can request that they are all the same via SFINAE

#include <type_traits>
#include <tuple>

template <typename ...T>
std::enable_if_t< 
    std::is_same_v< std::tuple<const char*, T...>,
                    std::tuple<T...,const char*>>
    ,void>
foo(T...t) {}

int main() {
    const char* x;
    int y;
    foo(x,x,x,x); // OK
    foo(x,y,x); // error
}

This is based on a neat trick to check if all types of a variadic pack are the same type (i'll add the referene to the original when I find it). std::tuple<const char*, T...> and std::tuple<T...,const char*> are only the same type when all Ts are const char*. std::enable_if will discard the specialization when the condition (all Ts are const char*) is not met and attempting to call it results in a compiler error.

This is rather old fashioned and works already in C++11 (apart from the _v/_t helpers). I suppose in C++20 there are less arcane ways to require all Ts to be const char*.


I missed that it is a constructor and you cannot do return-type-SFINAE on a constructor. It just needs to be a little more convoluted:

#include <type_traits>
#include <tuple>

struct foo {
    template <typename ...T,
              std::enable_if_t< 
                   std::is_same_v< std::tuple<const char*, T...>,
                                   std::tuple<T...,const char*>
                   >,
                   bool
              > = true>
    foo(T...t) {}
};

int main() {
    const char* x;
    int y;
    foo f1(x,x,x,x); // OK
    foo f2(x,y,x); // error
}

When the condition is met the last template parameter is non-type bool and has a default value of true. It's only purpose is to fail when the condition is not met (hence it does not need to be named).

463035818_is_not_an_ai
  • 109,796
  • 11
  • 89
  • 185
  • Would this method work for constructors in the header file as well? Adding `std::enable_if_t<...>` before the constructor makes the compiler complain that `'enable_if_t' in namespace 'std' does not name a template type`. I added all the required includes too, no idea what's wrong. – CryptoAlgorithm Aug 24 '22 at 13:41
  • @CryptoAlgorithm oh sorry. I missed that it is a constructor. Give me a second, I'll edit – 463035818_is_not_an_ai Aug 24 '22 at 13:43
  • *"I suppose in C++20 there are less arcane ways"*, as C++17 fold expression: `(std::is_same_v && ...)`. – Jarod42 Aug 24 '22 at 16:26
  • @Jarod42 or... `std::same_as auto...`? – Jeff Garrett Aug 24 '22 at 19:13
-1

You can't expand variadic arguments like parameter packs. You either have to switch to another approach (like the parameter packs/std::initializer_list) or rely on variadic functions of <cstdarg>:

Item::Item(const char* n, bool (*optionChange)(uint8_t), size_t opt_num, ...): name(n), type(MENU_TYPE_OPTS), selectedOpt(0) {
    va_list args;
    va_start(args, opt_num);
    
    for (decltype(opt_num) i = 0; i < opt_num; ++i) {
        const auto option = va_arg(args, const char *);
        std::cout << option << std::endl;
    }
    
    va_end(args);
    ...
}

Be advised that in this scenario, you don't have control over types of passed arguments, and the entire contract is supposed to be agreed verbally or in comments. The client code also should provide meta-data for the approach to be viable (i.e. in this example opt_num stores the number of strings passed)


EDIT

CPP community definitely doesn't like the varargs thing, as my answer was instantly downvoted, so another, more C++ friendly solution is to replace varargs with std::initializer_list:

Item::Item(const char* n, bool (*optionChange)(uint8_t), std::initializer_list<const char *> options): name(n), type(MENU_TYPE_OPTS), selectedOpt(0) {
    for (const auto& option: options){
        std::cout << option << std::endl;
    }
    ...
}
The Dreams Wind
  • 8,416
  • 2
  • 19
  • 49