2

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.

Nicol Bolas
  • 449,505
  • 63
  • 781
  • 982
j00hi
  • 5,420
  • 3
  • 45
  • 82
  • It's not possible with templates, as it's a compile-time only thing and the contents of the string is only known at runtime. You can however use a standard factory pattern, where the function returns a pointer to a common base-class, and you check the suffix at runtime to create the appropriate object. – Some programmer dude Jul 12 '19 at 08:27
  • Are the paths all known at compile time? If yes, there may be a solution... If no, it cannot work like this because types need to be known at compile time and thus cannot depend on something known at runtime only. – sebrockm Jul 12 '19 at 08:32
  • One could make overloads for paths which are known at compile-time. So for the sake of this question: Let's just assume that all paths are known at compile-time. – j00hi Jul 12 '19 at 08:36
  • The problem is that even literal strings aren't known at compile-time, or rather the *position* of them isn't known. That's why you can't use pointers to them either. The position of literal string constant arrays are only known at *link* time. – Some programmer dude Jul 12 '19 at 08:59
  • @Someprogrammerdude turns out string literals are not known at compile time, but char arrays with static linkage, apparently, are. See my answer. – sebrockm Jul 12 '19 at 12:51

2 Answers2

4

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.


Edit 2:

This issue has been fixed in VS 2019 version 16.4 (msvc v19.24): https://developercommunity.visualstudio.com/content/problem/341639/very-fragile-ice.html

See it working here.

Community
  • 1
  • 1
sebrockm
  • 5,733
  • 2
  • 16
  • 39
  • Wow, this looks very promising! Your proposed solution works for me up until the point where you introduce the macro. The error error which I get is "a template argument may not reference a non-external entity", meaning that I am not allowed to pass a non-global character array as a template parameter. I can only pass character arrays to the templates that are defined in global scope. Any ideas? (I'm using MSVC with the latest feature set enabled, i.e. C++17) – j00hi Jul 12 '19 at 13:10
  • @j00hi Unfortunately, I had tested it only with clang and gcc, but I can reproduce your error with msvc. I have updated my answer. I'm not 100% satisfied with it, but better than nothing, I guess. – sebrockm Jul 12 '19 at 14:01
  • 1
    @j00hi I asked another question about this issue: https://stackoverflow.com/q/57009060/9883438 Apparently, this is a bug in MSVC. So, maybe you are lucky and it will be fixed soon. – sebrockm Jul 12 '19 at 15:13
0

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 ?
max66
  • 65,235
  • 10
  • 71
  • 111
  • There are all kinds of things you can do at compile-time like sorting a `constexpr` declared array as shown in [constexpr initialization of array to sort contents](https://stackoverflow.com/questions/19559808/constexpr-initialization-of-array-to-sort-contents). So I was wondering if it is also possible to evaluate a string at compile-time. It would have to be a `constexpr` string. Maybe I should add this to my question. – j00hi Jul 12 '19 at 08:39
  • @j00hi: You still can’t have two different return types from the same function, and you can’t deduce a template argument that depends on more than the length of even a string literal. You could write a `constexpr` function of a string (literal) and use the result *as* a template argument, but you still can’t use the literal itself as a template argument to avoid duplication (although C++20 allows something mostly equivalent via the trick of wrapping it in a class-type object). – Davis Herring Jul 12 '19 at 09:46
  • @j00hi - there is a lot of things that you can do at compile-time but not have different return type for the same function. You can't have a `constexpr std::string` (at the moment) but you can have a `constexpr std::string_view` (from C++17) or a `consexpr` string literal (that is a `char const [someDim]`). But the problem is that you should pass the `constexpr` element as template parameter; this way the function signature change and you can have different functions with different return type. You can't pass a `constexpr` value as ordinary function argument changing the return type. – max66 Jul 12 '19 at 10:26