0

My goal for this code is to constrain a function parameter's passed value to a select few possibilities, being checked at compile time in C++20. My original broken first attempt looked something like this:

template<GLenum shader_type>
requires requires
{
    shader_type == GL_VERTEX_SHADER ||
    shader_type == GL_FRAGMENT_SHADER ||
    shader_type == GL_COMPUTE_SHADER;
}
GLuint create_shader(std::filesystem::path const& shader_path, GLenum shader_type)

This would not work, and I'm not going to argue with the compiler, but I'm including it here to help illustrate my aims for this declaration. My spec:

  1. Constrain a single passed GLenum to a set of three currently supported types. Preferably in an ad-hoc way, e.g. not requiring much boilerplate.
  2. For a user, be able to call this function like so: create_shader("my/path", GL_FRAGMENT_SHADER). No templates in sight.

The main issues with this code were:

  1. declaration of 'shader_type' shadows a template parameter. I can see this is because of the specification of GLenum shader_type in both the template parameters and function arguments, but I was hoping that somehow have the compiler might infer the type from function args.
  2. shader_type == GL_VERTEX_SHADER || shader_type == GL_FRAGMENT_SHADER || shader_type == GL_COMPUTE_SHADER is not doing it's job at all. I could pass GL_GEOMETRY_SHADER in with no compile-time issue. (I wish I could say the same for run-time)

After looking at this Question I came up with this next:

template<GLenum shader_type>
requires requires
{
    requires shader_type == GL_VERTEX_SHADER ||
             shader_type == GL_FRAGMENT_SHADER ||
             shader_type == GL_COMPUTE_SHADER;
}
GLuint create_shader(std::filesystem::path const& shader_path)

This seems to work as I desire when it comes to constraining the passed value to those values, however, it does require shader_type being passed in as a template parameter. This is fine really, but it leads me to my questions:

  1. Is there a way to do as I've done in terms of constraints but without having the user pass in shader_type as a template parameter explicitly, instead preferring a function argument?
  2. Is there a way of writing this declaration with fewer requires? I suspect not, but it would be nice if I can cut one out.
  3. Generally, does anyone know any alternatives which are "better" on the whole. Perhaps referring to my spec at the top of this question for what "better" might be.

Appendix:

  1. I'm aware of constexpr and that I could probably do the check inside the function, but doing it through constraints and concepts is also a bit of exploration for me, as a learner.
  2. In case people are confused by GLenum or any of the other non-standard code:
typedef unsigned int GLenum;
typedef unsigned int GLuint;
#define GL_FRAGMENT_SHADER 0x8B30
#define GL_VERTEX_SHADER 0x8B31
#define GL_COMPUTE_SHADER 0x91B9
康桓瑋
  • 33,481
  • 5
  • 40
  • 90
Cascades
  • 627
  • 7
  • 18

2 Answers2

1

If you wanted to constrain a template parameter, a single requires is enough:

template <GLenum shader_type>
requires(shader_type == GL_VERTEX_SHADER || shader_type == GL_FRAGMENT_SHADER)
void foo() {}

A function parameter can be constrained like this, at the cost of requiring it to be a compile-time constant:

struct ShaderType
{
    GLenum value;
    consteval ShaderType(GLenum value) : value(value)
    {
        if (shader_type != GL_VERTEX_SHADER && shader_type != GL_FRAGMENT_SHADER)
            throw "Invalid value!";
         // Normally it's not a good idea to throw pointers,
         // but here it just used to stop the compilation.
    }
};

void foo(ShaderType type) {}
HolyBlackCat
  • 78,603
  • 9
  • 131
  • 207
  • Can't you just have a `constexpr` function that returns a boolean, then call that as part of the `requires` clause? `consteval` seems unnecessary here. – Nicol Bolas Feb 19 '22 at 17:46
  • @NicolBolas I was thinking that function parameters are never considered constexpr, and wouldn't work there. – HolyBlackCat Feb 19 '22 at 18:53
  • A `consteval` constructor can only be called at compile-time. So it is impossible to create such an object at runtime from a runtime value. So while a `ShaderType` object can pass through non-constexpr code, it must always *start* from a compile-time constant. At which point, passing through non-constexpr code is pointless, so you may as well take the enumerator directly as a template parameter. – Nicol Bolas Feb 19 '22 at 18:55
  • @NicolBolas I agree, hence the first snippet the answer. The `consteval` trick is there because OP asked for it, and I respect the long-standing SO tradition of explaining how to shoot oneself in the foot if OP wants it. :P – HolyBlackCat Feb 19 '22 at 19:01
  • I opted to to go for the top code snippet. – Cascades Feb 20 '22 at 01:33
  • How would one create a concept definition from this? – Sean Mar 27 '22 at 17:23
  • @Sean Concepts accept a boolean expression, so `template concept Shader = shader_type == GL_VERTEX_SHADER || shader_type == GL_FRAGMENT_SHADER;`. But concepts with the first parameter not being a type parameter don't support the syntax shortands, so you still need to write `requires Shader` rather than putting the concept into the template argument list. – HolyBlackCat Mar 27 '22 at 17:27
1

You cannot constrain a runtime value (like a function parameter) with a compile-time construct (like a requires clause). What you want is not a compile-time constraint; it's a runtime contract.

C++ does not as of yet have such a feature. The best you can do at present is use a proper enumeration (or typed static constexpr variables) that only permits certain values.

Nicol Bolas
  • 449,505
  • 63
  • 781
  • 982