Search code examples
c++constructorconstexprcompile-time

C++ constexpr constructor for colours


I have a class Colour:

class Colour {
public:
  std::byte r;
  std::byte g;
  std::byte b;
  std::byte a;
};

Now if I have a function

void foo(const Colour& c);

I want to be able to call it by passing a string that represents the colour:

foo("red"); // become (255, 0, 0, 255)
foo("#00ff00"); // become (0, 255, 0, 255)
foo("hsl(240, 100, 50)"); // become (0, 0, 255, 255)

And of course I don't want to parse the string everytime, I would like the compiler to parse it and replace the string by the colour.

The problem is that we constexpr constructors, it can't have a body and must directly initialize the r, g, b, a values, or I could have a private member colour so I can initialize it like this:

class Colour {
public:
  constexpr Colour(const std::string& str) : colour(parseString(str)) {}

private:
  InternalColor colour; // contains the r, g, b, a
};

constexpr InternalColour parseString(const std::string& str) {
  // ...
  // Parse the string and return the colour
}

But I want to have access to the r, g, b, a values directly, not with an indirection, and I don't want member functions like r().

So how can I parse the colour string at compile time and replace it with the colour? And yes I could call myself a constexpr function that return the color, but the idea is to directly pass a string.


Solution

  • I don't want to parse the string everytime,

    Even with a constexpr InternalColour parseString(const std::string_view& str)

    None of the calls are in constant expressions, so apply at runtime (optimizer might help):

    foo("red"); // become (255, 0, 0, 255)
    foo("#00ff00"); // become (0, 255, 0, 255)
    foo("hsl(240, 100, 50)"); // become (0, 0, 255, 255)
    

    You would have to do:

    constexpr Colour red{"red"}; // Parsed at compile time
    foo(red);
    // ...
    

    with constexpr constructors, it can't have a body

    C++11 rules are really strict. :/

    Since C++14 rules have been relaxed a lot.

    Even in C++11, you might use delegating constructor to by-pass that issue.

    class Colour {
    public:
      constexpr Colour(std::uint8_t r, std::uint8_t g, std::uint8_t b, std::uint8_t a) :
          r(r), g(g), b(b), a(a) {}
      // not `explicit`, as you want implicit conversion
      constexpr Colour(const std::string_view& str) : Colour(parseString(str)) {}
    
      constexpr Colour(const Colour& rhs) = default;
      constexpr Colour& operator= (const Colour& rhs) = default;
    
    public:
        static constexpr Colour parseString(const std::string_view& str)
        {
            // constexpr parsing in C++11 might be non trivial,
            // but possible (one return statement only :/ )
    
            if (str == "red") { return {255, 0, 0, 255}; }
            // ...
        }
    
    public:
        std::uint8_t r;
        std::uint8_t g;
        std::uint8_t b;
        std::uint8_t a;
    };