I can not get my head around the following problem. I don't even really know how I could approach it.
Consider this code:
struct fragment_shader {
std::string mPath;
};
struct vertex_shader {
std::string mPath;
};
template <typename T>
T shader(std::string path) {
return T{ path };
}
To create the different structs, I can write the following:
auto fragmentShader = shader<vertex_shader>("some_shader.frag");
auto vertexShader = shader<fragment_shader>("some_shader.vert");
I am wondering, if it is possible to let the compiler figure out the type based on the path
parameter which is passed to the shader
function, so I would only have to write:
auto fragmentShader = shader("some_shader.frag");
auto vertexShader = shader("some_shader.vert");
and because of the file ending ".frag", the type fragment_shader
would be inferred, and for a path ending with ".vert", vertex_shader
would be inferred.
Is that possible?
I was reading up a bit on enable_if
, but actually I have no idea how I could use that to achieve what I am trying to achieve. I would try something like follows:
template<>
typename std::enable_if<path.endsWith(".frag"), fragment_shader>::type shader(std::string path) {
return fragment_shader{ path };
}
template<>
typename std::enable_if<path.endsWith(".vert"), vertex_shader>::type shader(std::string path) {
return vertex_shader{ path };
}
But obviously, this doesn't compile. It's just to make clear what I am trying to do.
If all paths are known at compile time, I have a solution. It turns out that fixed size char
arrays that are declared with static linkage can be used as template arguments (as opposed to string literals), and thus you can make a function return two different types depending on that template argument:
This is a helper function that can determine at compile time if the file ending is .frag
(you may want to have an equivalent function for .vert
):
template <std::size_t N, const char (&path)[N]>
constexpr bool is_fragment_shader()
{
char suf[] = ".frag";
auto suf_len = sizeof(suf);
if (N < suf_len)
return false;
for (int i = 0; i < suf_len; ++i)
if (path[N - suf_len + i] != suf[i])
return false;
return true;
}
This function returns two different types depending on the file ending. As you tagged the question with C++17
, I used if constexpr
instead of enable_if
which I find much more readable. But having two overloads via enable_if
will work, too:
template <std::size_t N, const char (&path)[N]>
auto shader_impl()
{
if constexpr (is_fragment_shader<N, path>())
return fragment_shader{ path };
else
return vertex_shader{ path };
}
And finally, to use it, you need to do this:
static constexpr const char path[] = "some_shader.frag"; // this is the important line
auto frag = shader_impl<sizeof(path), path>();
This is of course a little annoying to write. If you are OK with using a macro, you can define one that defines a lambda holding the static string and executes that immediately like so:
#define shader(p) \
[]{ \
static constexpr const char path[] = p; \ // this is the important line
return shader_impl<sizeof(path), path>(); \
}() \
Then the call syntax is just as you want it:
auto frag = shader("some_shader.frag");
static_assert(std::is_same_v<decltype(frag), fragment_shader>);
auto vert = shader("some_shader.vert");
static_assert(std::is_same_v<decltype(vert), vertex_shader>);
Please find a fully working example here.
Edit:
As it turns out that MSVC only allows char
arrays as template arguments if they are declared in the global namespace, the best solution I can think of is to declare all needed paths just there.
static constexpr char some_shader_frag[] = "some_shader.frag";
static constexpr char some_shader_vert[] = "some_shader.vert";
If you slightly alter the macro, the calls can still look quite nice (although having to declare the strings elsewhere remains being a big PITA, of course):
#define shader(p) \
[]{ \
return shader_impl<sizeof(p), p>(); \
}() \
void test()
{
auto frag = shader(some_shader_frag);
static_assert(std::is_same_v<decltype(frag), fragment_shader>);
auto vert = shader(some_shader_vert);
static_assert(std::is_same_v<decltype(vert), vertex_shader>);
}
See it working here.
Is that possible?
Short answer: no.
Long answer.
C++ is a statically typed language and the compiler require to decide the returned type of a function at compile time.
In your case with
auto fragmentShader = shader("some_shader.frag");
auto vertexShader = shader("some_shader.vert");
you're trying to obtain two different return types from the same function and decide the return type from a run-time known value.
I know that "some_shader.frag"
is a char const [17]
known at compile time, but the problem is that shader()
receive also std::string
known only at run-time
std::string s;
std::cin >> s;
auto foo = shader(s); // which type, in this case, at run-time ?