0

Assume all I need is a type Color inhabited by only 3 values, red, green, blue, (just like bool is inhabited just by 2 values, true and false), and the ability to tell those 3 values apart from each other, i.e. the ability to test that e.g. red == red and red != blue are both true.

In Haskell, such a type would be define with a one-liner:

data Color = Red | Green | Blue deriving Eq -- ctors have a capital first letter

but what about C++?

I've learned that enums can be tricky, so I was thinking of using std::variant for the job.

The best I could come up with is the following:

namespace colors {

namespace detail {
inline constexpr struct Red{} red;
inline constexpr struct Green{} green;
inline constexpr struct Blue{} blue;
}

using detail::red;
using detail::green;
using detail::blue;

using Color = std::variant<detail::Red, detail::Green, detail::Blue>;

inline constexpr bool operator==(Color const& first, Color const& second) {
    return first.index() == second.index();
}

inline constexpr bool operator!=(Color const& first, Color const& second) {
    return !(first == second);
}

}

where I've enclosed the 3 types in the detail namespace only to not clutter code completion lists of an IDE, as the client code would rarely need those 3 types, and will most often just use the 3 variables.

To me, using such a type seems seamless and convenient:

#include <cassert>
#include "the-file-above.hpp"
int main() {
    using namespace colors;

    Color color;
    assert(color == red);

    color = green;
    assert(color == green);

    color = blue;
    assert(color == blue);

    color = red;
    assert(color == red);
}

Are there any objective reason why I should prefer the most basic enum approach to this one, in terms of performance?

Enlico
  • 23,259
  • 6
  • 48
  • 102
  • 6
    In general use the most readable option available, until you have proven (by using a profiler) that whatever solution you used will be your bottleneck... And I apologize if I was the one confusing you with respect to enums. Enums are ok to use as long as you are not casting them to and from integers that are out of range (or have no meaning) directly. – Pepijn Kramer Jan 30 '23 at 08:48
  • @PepijnKramer, no confusion. I just see that I might not be the one to make the cast, but somebody else might do. With the variant solution, it doesn't seem possible at all to misuse it. From the perspective of the includer of the variant-based file, there's only 4 words readily available, `Color`, `red`, `green`, `blue`, they're not usable in place of any other type, and they can be checked for equality. To me it looks most usable, and the `main` most readable. – Enlico Jan 30 '23 at 08:52
  • 8
    Well, if "someone else" makes the cast then nothing prevents that "someone else" from making something like `inline constexpr struct Red{} green;` and messing things up. I usually consider efforts aimed at making C++ code completely fullproof to be rather worthless. – user7860670 Jan 30 '23 at 09:04
  • 1
    The enum itself is safe enough, plus can have underlying type and flag-like enumerators. Someone can consider underlying type and/or flag-like enumerators as "misuse" other can consider those as great performance and portability advantages. – Öö Tiib Jan 30 '23 at 09:19
  • @user7860670, but that would be just a synonym. With a poorly chosen name, but it would still be a `Red`, not a new animal that one doesn't even expect to be there, for intsance. – Enlico Jan 30 '23 at 09:23
  • 5
    @user7860670 What was that quote again? "It is hard to make anything foolproof, since fools are so damn ingenious." So yes I agree it is too hard., then again I usually add some input checking (as long as it is cheap). – Pepijn Kramer Jan 30 '23 at 10:30
  • @pepijnKramer So we foolproof the dumb fools only :) – Passer By Jan 30 '23 at 10:49
  • 2
    Aside: I'd write `inline constexpr auto operator==(Red, Red) = default;` etc. in your detail namespace rather than specifically define the variant ones – Caleth Jan 30 '23 at 11:59
  • @Caleth, would the benefit of that the fact that I don't have to write the body of the function myself, or there would be other benefits? – Enlico Jan 30 '23 at 12:27
  • 1
    @Enlico I think it communicates intent better. – Caleth Jan 30 '23 at 12:43
  • Actually, @Caleth, I might have misunderstood your suggestion. I cannot default, say, `opearator==(Red, Green)`, because the two args' types differ; plus, even defining `operator==(Red,Red)` and the other 2, how is that gonna help when checking `someColor == red`? – Enlico Jan 30 '23 at 13:47
  • 1
    @Enlico oh, my mistake, to default it, you need to define it inside the class (or as a friend). There are comparison operators for std::variant that are defined in terms of comparing the held values. [An example](https://coliru.stacked-crooked.com/a/8d1a6a0561b0f68f) – Caleth Jan 30 '23 at 14:14
  • Wow. I see now. What a gross mistake defining my own `operator==` for a `std::` thing :( – Enlico Jan 31 '23 at 09:33
  • @BenVoigt, I guess if I'm stuck on C++17 and don't need other than equality/inequality, I can limit myself to define `operator==` (and `operator!=` in terms of it) for `Red`-`Red`, `Green`-`Green`, and `Blue`-`Blue`? – Enlico Jan 31 '23 at 09:36
  • @Enlico: Actually I was wrong, argument dependent lookup does consider the namespaces associated not only with `std::variant` but also it's type template arguments. https://eel.is/c++draft/basic.lookup.argdep#3.2 – Ben Voigt Jan 31 '23 at 15:19

0 Answers0