SFINAE/enable_if 基于字符串参数的内容?

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

我无法解决以下问题。我什至不知道该如何处理它。

考虑这段代码:

struct fragment_shader {
    std::string mPath;
};

struct vertex_shader {
    std::string mPath;
};

template <typename T>
T shader(std::string path) { 
    return T{ path };
}

要创建不同的结构,我可以编写以下内容:

auto fragmentShader = shader<vertex_shader>("some_shader.frag");
auto vertexShader = shader<fragment_shader>("some_shader.vert");

我想知道,是否可以让编译器根据传递给 shader 函数的 path 参数找出类型,所以我只需要写:

auto fragmentShader = shader("some_shader.frag");
auto vertexShader = shader("some_shader.vert");

并且由于文件以“.frag”结尾,类型将被推断为 fragment_shader,对于以“.vert”结尾的路径,将推断出 vertex_shader

这可能吗?

我阅读了一些关于 enable_if 的内容,但实际上我不知道如何使用它来实现我想要实现的目标。我会尝试如下操作:

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 };
}

但显然,这无法编译。这只是为了弄清楚我要做什么。

Is that possible?

简答:没有。

长答案。

C++ 是静态类型语言,编译器需要在编译时决定函数的 returned 类型。

你的情况是

auto fragmentShader = shader("some_shader.frag");
auto vertexShader = shader("some_shader.vert");

您正在尝试从同一函数获取两种不同的 return 类型,并根据 run-time 已知值确定 return 类型。

我知道 "some_shader.frag" 是编译时已知的 char const [17],但问题是 shader() 也接收 std::string 仅在 run-time 时已知

std::string s;

std::cin >> s;

auto foo = shader(s);  // which type, in this case, at run-time ?

如果在编译时所有路径都已知,我有一个解决方案。事实证明,使用静态链接声明的固定大小 char 数组可以用作模板参数(与字符串文字相反),因此您可以根据需要使函数 return 成为两种不同的类型模板参数:

这是一个辅助函数,可以在编译时确定文件结尾是否为 .frag(您可能需要 .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;
}

此函数 return 有两种不同的类型,具体取决于文件结尾。当您用 C++17 标记问题时,我使用 if constexpr 而不是 enable_if,我发现后者更具可读性。但是通过 enable_if 进行两次重载也可以工作:

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 };
}

最后,要使用它,您需要执行以下操作:

static constexpr const char path[] = "some_shader.frag"; // this is the important line
auto frag = shader_impl<sizeof(path), path>();

这样写当然有点烦。如果您可以使用宏,则可以定义一个定义一个包含静态字符串的 lambda 并立即执行的宏,如下所示:

#define shader(p) \
[]{ \
    static constexpr const char path[] = p; \ // this is the important line
    return shader_impl<sizeof(path), path>(); \
}() \

那么调用语法就如你所愿:

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>);

请找到一个完整的示例 here


编辑:

事实证明,MSVC 只允许 char 数组作为模板参数,如果它们是在全局命名空间中声明的话,我能想到的最好的解决方案是在那里声明所有需要的路径。

static constexpr char some_shader_frag[] = "some_shader.frag";
static constexpr char some_shader_vert[] = "some_shader.vert";

如果您稍微改变宏,调用看起来仍然相当不错(当然,必须在别处声明字符串仍然是一个很大的 PITA):

#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>);
}

看到它工作 here


编辑 2:

此问题已在 VS 2019 版本 16.4 (msvc v19.24) 中修复:https://developercommunity.visualstudio.com/content/problem/341639/very-fragile-ice.html

看到它工作 here