3

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++ ?

Makogan
  • 8,208
  • 7
  • 44
  • 112
  • std::initializer_list? – DeiDei May 20 '18 at 21:59
  • "I need to pass from a minimum of one to a maximum of seven file paths to a function." - the way of doing this since time immemorial is to pass a string containing all the paths and have your function parse out the individual paths. Or better, don't design applications that do things like this. –  May 20 '18 at 22:03
  • How else would you propose handling a program that can potentially have from one to seven different files passed onto it then? – Makogan May 20 '18 at 22:04
  • 3
    I would ask why you have 1 to 7 files being passed to it. –  May 20 '18 at 22:05
  • You could modify your function so that it accepts a vector of strings. Then you could pass as many as you want. Would this work? Or do you need to enforce at compile time that the paths are at most seven? – Fabio says Reinstate Monica May 20 '18 at 22:07
  • Because OpenGL has up to 7 shader types (vertex, tesellation control/evaluation, geometry...) But not all shaders are necessary, so you can define only one or two or just three, and all of them are optional. My function compiles a shading program, so I can be given anywhere from 1 to all of the sahders – Makogan May 20 '18 at 22:07
  • What's wrong with collecting them into a wrapper container - like you mention it, a `vector`, `initialiser_list` or something like that? I guess this would be a far better solution than fiddling around with empty arguments, parsing strings or even C-style variable argument lists... – Michael Beer May 20 '18 at 22:08
  • 3
    Pass a single `struct` describing the data, then inspect the struct. That keeps the number of parameters to 1. That also is the way you pass something "complex" that has various pieces to it that needs to be interpreted in some way. – PaulMcKenzie May 20 '18 at 22:08
  • @FabioTurati, As you can read in my question, I am aware one can use arrays/vectors to passa list of strings as arguments, but i'd rather pass the parameters directly (no use of arrays) if possible – Makogan May 20 '18 at 22:08
  • @MichaelBeer there's nothing wrong with it, that is a possible way fo doing it, I simply have a personal preference (regardless of how hard it may be to implement it) to not pass an array to this function if I can – Makogan May 20 '18 at 22:09
  • @Makogan: Having an abundant amount of function arguments to pass is not considered good practice IMHO. Better wrap them into some container. Or write a wrapper struct, as suggested by PaulMcKenzie . From my experience with legacy code, long argument lists tend to obscure the code. – Michael Beer May 20 '18 at 22:12
  • It depends on what you are doing. See for example how hardware interaction functions work, usually you have quite a bit of parameters (for example OpenGL has multiple functions that take more than 5 arguments). I understand and appreciate the criticism, but I abide by this specific design choice. – Makogan May 20 '18 at 22:14
  • @Makogan -- Well as I stated, if you want to keep the number of parameters 1, then keep the number of parameters 1. The trick is to make the single parameter "rich", and that is accomplished by setting up a `struct` that has the interface as to what functions to call. – PaulMcKenzie May 20 '18 at 22:18
  • I don't want to keep it at one, I want to allow it to vary, and then try to use the compiler to define how the data ought to be handled – Makogan May 20 '18 at 22:26
  • @Makogan: I would advise you to [read this answer](https://stackoverflow.com/a/11068591/734069). The more parameters your function has, the less intuitive it will be to use. Also, it makes no sense to mix compute shaders with non-compute shaders, so your function should have 6 parameters, not 7. – Nicol Bolas May 20 '18 at 22:32
  • @NicolBolas I think you may have close to 100% participation in my posts in SO by now :P – Makogan May 20 '18 at 22:39

3 Answers3

4

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);
}
super
  • 12,335
  • 2
  • 19
  • 29
3

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(...)
        ...
    ;
    
Acorn
  • 24,970
  • 5
  • 40
  • 69
  • 1
    @Makogan: No problem and you're welcome! I agree, it does not answer the question. However, many times when someone answers with the actual, factual answer is criticized for not providing "better" ways of doing the solution -- so I wanted to acknowledge super's solution (for C++11) and add those alternative ways. – Acorn May 20 '18 at 22:25
  • @Makogan: I have expanded the answer to provide a range of possibilities depending on what is needed to do. – Acorn May 20 '18 at 23:52
  • 1
    I am conflicted, your answer is very well documented and is more likely to help a larger audience than super's, however super;s is one to one the answer to what was asked in a very nice self contained and copypastable format. I am not sure which answer to accept as the answer :p – Makogan May 20 '18 at 23:56
  • @Makogan: It depends on how you look at it! :-) Short and to the point answers are great, I agree; specially for people looking for a fast answer. However, also note that the question does not specify C++11 and that, as given, it is quite broad. In any case, do not worry too much -- you can always decide later on. – Acorn May 21 '18 at 00:12
  • @Makogan: By the way, if this is for a function that compiles shaders, how do you know what value to pass to `glCreateShader()`? In other words, how do you know which argument is which shader? Do the arguments (`fileN`) know the type of shader they contain? (I guess so, since you say all the parameters are of the same type). – Acorn May 21 '18 at 00:15
  • 1
    I simply forced the convention that the shader name (the file name) needs to have as a substring the type (e.g Basic-Fragment.glsl) (not case sensitive) and I determine the shader type that way. – Makogan May 21 '18 at 00:16
  • @Makogan: I see. In that case, the most straightforward way is an `std::initializer_list`, because you can simply iterate through it. If you use variadic macros, then you will have to play a bit in order to pass around some state while you do the calls. – Acorn May 21 '18 at 00:32
0

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.

Nicol Bolas
  • 449,505
  • 63
  • 781
  • 982