用于OpenGL调用的c++11包装器

c++11 wrappers for OpenGL calls

本文关键字:包装 c++11 OpenGL 调用 用于      更新时间:2023-10-16

大多数OpenGL API调用都不会返回值。有了C++11完美的转发,就可以很容易地编写一个错误检查包装器函数模板和可变宏,比如

template <typename F, typename ...Args>
void call(
    const char *text,
    int line,
    const char *file,
    F && f, Args &&... args
    )
{
    std::forward<F>(f)(std::forward<Args>(args)...);
    auto err = glGetError();
    if (err != 0) _throw_error(text, err, line, file);
}
#define GL(fn, ...) call(#fn, __LINE__, __FILE__, gl##fn, __VA_ARGS__)

这是有效的,并允许调用这样写:

GL(Viewport, 0, 0, 1920, 1080);

但是,一些OpenGL函数确实返回了一个值,这使得包装函数在检查错误之前必须将返回值存储在临时变量中。带有返回值的调用的包装器函数模板可以这样写:

template <typename F, typename ...Args>
auto call(
    const char *text,
    int line,
    const char *file,
    F && f, Args &&... args
    )
{
    auto && res = std::forward<F>(f)(std::forward<Args>(args)...);
    auto err = glGetError();
    if (err != 0) _throw_error(text, err, line, file);
    return res;
}

(相同的宏可用于调用此表单。)

问题是,无法定义上述函数模板中的两个,因为对于所有返回类型为void的API调用,两个模板都将匹配,从而导致"对重载函数的调用不明确"错误(Visual C++)。

当然,可以为这两种形式定义单独的宏,但这并不优雅。GL是包装宏名称的自然、明显和正确的选择,任何额外的宏(如GLR)都会显得很难看,很难记住。

特别是在发布版本中,您不希望在每次调用后都调用glGetError。这对性能没有好处,而且glGetError无论如何都不会返回描述性错误(GL_INVALID_OPERATION用于很多事情)。

相反,为了进行调试,请创建一个调试上下文,并使用glDebugMessageCallback​注册一个回调,该回调将打印出错误消息。这是一个相对较新的OpenGL功能,但根据我的经验,它提供了更好的消息。

void debug_callback(GLenum source​, GLenum type​, GLuint id​,
    GLenum severity​, GLsizei length​, const GLchar* message​, const void* userParam) {
    printf("%s", message);
}
glDebugMessageCallback​(&debug_callback​, nullptr​);

上述问题的解决方案并不困难,但我惊讶地发现,Visual C++不会接受我使用Coliru(使用gcc)制定并验证的解决方案(这意味着它被内部编译器错误卡住了)。

当然,需要的是SFINAE,而C++11通过非常方便的std::enable_if<>支持它。适用于gcc但不适用于Visual Studio 2015的解决方案是:

template <typename F, typename ...Args,
    typename R = std::enable_if_t<!std::is_void<std::result_of_t<F(Args...)>>::value, std::result_of_t<F(Args...)>>>
>
R call(const char *text, int line, const char *file, F && f, Args &&...args)
{...}

(类似于void函数,使用std:is_void<...>::value)。

经过一番搜索,我找到了以下可以编译的内容:

template <typename F, typename ...Args>
auto call(
    std::enable_if_t<!std::is_void<std::result_of_t<F(Args...)>>::value, const char> *text,
    int line,
    const char *file,
    F && f, Args &&... args
    )
{
    auto && res = std::forward<F>(f)(std::forward<Args>(args)...);
    auto err = glGetError();
    if (err != 0) _throw_error(text, err, line, file);
    return res;
}

template <typename F, typename ...Args>
void call(
    std::enable_if_t<std::is_void<std::result_of_t<F(Args...)>>::value, const char> *text,
    int line,
    const char *file,
    F && f, Args &&... args
    )
{
    std::forward<F>(f)(std::forward<Args>(args)...);
    auto err = glGetError();
    if (err != 0) _throw_error(text, err, line, file);
}

这个解决方案唯一值得注意的是它使用SFINAE的方式。std::enable_if_t<>不仅用于消除不必要的专门化,而且还声明了宏传入的text参数的类型。因此,我没有通过一个带有默认值的额外的、未使用的参数来执行SFINAE(我尝试过,但由于某种原因,在Visual Studio下也会导致"内部编译器错误"),而是在我无论如何都需要的三个额外参数中的一个上使用了它。