Search code examples
c++c++17sfinaeenable-if

SFINAE/enable_if based on the contents of a string parameter?


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.


Solution

  • 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.