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::nullopt
s 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.