Makogan
Makogan

Reputation: 9632

Can you pass a varying number of arguments of the same type to a function without using arrays?

I need to pass from a minumum fo one to a maximum of seven file paths to a function. There is a convention where the file path alone is enough to identify how to handle each file.

Order of the parameters does not matter.

An obvious option to handle this (the one I currently implemented) is to pass an empty string as a parameter for unused slots.

Another one is to pass the parameters as an array or vector.

Yet another one would be to implement all possible permutations of parameters (possible, not practical).

I wonder if there is a way to simply specify that the number of paramters can vary, and then simply pass the parameters themselves.

So for example assuming that there is only one implementation of f() with special syntax to denote varying amounts of parameters

All fo the following should compile:

int main()
{
   f(file);
   f(file1, file2);
   f(file1, file3, file2, file6);
}

Is there a way to achieve this in C++ ?

Upvotes: 3

Views: 532

Answers (3)

Acorn
Acorn

Reputation: 26196

If you really need a variable (unbounded) number of arguments:

Otherwise, if you have a fixed number of (optional) parameters:

  • If you are on C++20 or later:

  • If you are on C++03 or later:

    • Use a nullable/optional type (e.g. a raw pointer, boost::optional, C++17's std::optional...) -- see @NicolBolas' answer.

    • Define all required/logical overloads (possibly using custom types) -- ugly, but this may be automated via an external code generator and/or with the preprocessor.

Otherwise, if you can use a different design to accomplish the same thing, you can do any of the following -- for C++03 and later:

  • Pass a pointer to a struct as suggested by @PaulMcKenzie.

  • Design a class that allows to set properties (through the constructor and/or methods) and then has member functions to perform operations on that data, e.g.:

    ShaderCompiler sc(vs, fs, ...);
    sc.setGeometryShader(...);
    sc.compile();
    
  • A particular nice way (see e.g. QString) is to design a class that allows to do:

    result = ShaderCompiler()
        .vertex(...)
        .fragment(...)
        ...
        .compile()
    ;
    
  • Similarly, exploiting argument-dependent lookup:

    Shader()
        << Vertex(...)
        << Fragment(...)
        ...
    ;
    

Upvotes: 3

Nicol Bolas
Nicol Bolas

Reputation: 474316

Since you have a bounded set of possibilities, here's the obvious way to handle this:

using opt_path = std::optional<path>;

shader compile_shaders(opt_path vs, opt_path tcs = std::nullopt, opt_path tes = std::nullopt, opt_path gs = std::nullopt, opt_path fs = std::nullopt, opt_path cs = std::nullopt)
{
  ...
}

These just use default arguments for all other shader paths. You can tell which is provided and which is not through the interface to std::optional. If you're not using C++17, you'll obviously substitute that for boost::optional or a similar type.

Of course, however you handle this, it will lead to a decidedly poor interface. Consider what one has to do in order to create the most common case: a vertex shader combined with a fragment shader:

compile_shaders(vs_path, std::nullopt, std::nullopt, std::nullopt, fs_path);

Will the user remember that there are 3 stages between them? Odds are good, they will not. People will constantly make the mistake of using only 2 std::nullopts or using 4. And considering that VS+FS is the most common case, you have an interface where the most common case is very easy to get wrong.

Now sure, you could rearrange the order of the parameters, making the FS the second parameter. But if you want to use other stages, you now have to look up the definition of the function to remember which values map to which stages. At least, the way I did it here follows OpenGL's pipeline. An arbitrary mapping requires looking up the docs.

And if you want to create a compute shader, you have to remember there are 6 stages you have to explicitly null out:

compile_shaders(std::nullopt, std::nullopt, std::nullopt, std::nullopt, std::nullopt, std::nullopt, cs_path);

Compare all this to a more self-descriptive interface:

shader_paths paths(vs_path, shader_paths::vs);
paths.fragment(fs_path);
auto shader = compile_shaders(paths);

There is zero ambiguity here. The path given to the constructor is explicitly stated to be a vertex shader, using a second argument. So if you want a compute shader, you would use shader_paths::cs to express that. The paths are then given a fragment shader, using an appropriately named function. Following this, you compile the shaders, and you're done.

Upvotes: 0

super
super

Reputation: 12968

You can use a recursive template function.

#include <iostream>

template <typename First>
void f(First&& first) {
    std::cout << first << std::endl;
}

template <typename First, typename... Rest>
void f(First&& first, Rest&&... rest) {
    f(std::forward<First>(first));
    f(std::forward<Rest>(rest)...);
}

int main() {
    f(6,7,8,9,10);
}

Upvotes: 4

Related Questions