1
\$\begingroup\$

This is meant to go in a vulkan.hpp-like strongly-typed wrapper for a pure C library.

A wrapped enum provides back and forth implicit conversion between itself and the enum, while otherwise behaving as an enum class would.

Wrapper utility:

#include <type_traits>

template <typename EnumT>
struct enum_wrapper {
  using enum_type = EnumT;
  using wrapped_type = enum_wrapper<EnumT>;

  enum_wrapper() noexcept = default;
  ~enum_wrapper() = default;

  // Implicitly castable from enum_type
  constexpr enum_wrapper(enum_type val) noexcept : _value(val) {}

  // Implicitly castable to enum_type
  constexpr operator enum_type() const noexcept { return _value; }

  // Because of the implicit conversion, we need to intercept all comparisons
  // in order to prevent comparing against an unrelated enum.
  template <typename T>
  constexpr bool operator<(T rhs) const noexcept {
    static_assert(is_comparable<T>());
    return _value < rhs;
  }

  template <typename T>
  constexpr bool operator<=(T rhs) const noexcept {
    static_assert(is_comparable<T>());
    return _value <= rhs;
  }

  template <typename T>
  constexpr bool operator==(T rhs) const noexcept {
    static_assert(is_comparable<T>());
    return _value == rhs;
  }

  template <typename T, std::enable_if_t<std::is_enum_v<T>, int> = 0>
  constexpr bool operator!=(T rhs) const noexcept {
    static_assert(is_comparable<T>());
    return _value != rhs;
  }

  template <typename T>
  constexpr bool operator>(T rhs) const noexcept {
    static_assert(is_comparable<T>());
    return _value < rhs;
  }

  template <typename T>
  constexpr bool operator>=(T rhs) const noexcept {
    static_assert(is_comparable<T>());
    return _value <= rhs;
  }

 private:
   enum_type _value;

  template <typename T>
  static constexpr bool is_comparable() {
    return std::is_same_v<std::decay_t<T>, enum_type> || std::is_same_v<std::decay_t<T>, enum_wrapper>;
  }
};

template <typename LhsT, typename EnumT>
constexpr bool operator<(LhsT lhs, enum_wrapper<EnumT> rhs) noexcept {
  return rhs > lhs;
}

template <typename LhsT, typename EnumT>
constexpr bool operator<=(LhsT lhs, enum_wrapper<EnumT> rhs) noexcept {
  return rhs >= lhs;
}

template <typename LhsT, typename EnumT>
constexpr bool operator==(LhsT lhs, enum_wrapper<EnumT> rhs) noexcept {
  return rhs == lhs;
}

template <typename LhsT, typename EnumT>
constexpr bool operator!=(LhsT lhs, enum_wrapper<EnumT> rhs) noexcept {
  return rhs != lhs;
}

template <typename LhsT, typename EnumT>
constexpr bool operator>(LhsT lhs, enum_wrapper<EnumT> rhs) noexcept {
  return rhs < lhs;
}

template <typename LhsT, typename EnumT>
constexpr bool operator>=(LhsT lhs, enum_wrapper<EnumT> rhs) noexcept {
  return rhs <= lhs;
}
}

Wrapping:

extern "C" {
enum lib_status {
  LIB_STATUS_OK = 0,
  LIB_STATUS_BAD_ARGUMENT = 1,
};
typedef enum lib_status lib_status;

enum lib_color {
  LIB_COLOR_RED = 0,
  LIB_COLOR_GREEN = 1,
  LIB_COLOR_BLUE = 2,
};
typedef enum lib_color lib_color;

lib_status lib_foo(lib_color color);
}

namespace lib {

  // <insert copy of lib_color documentation>
  struct color : detail::enum_wrapper<lib_color> {
    using enum_wrapper::enum_wrapper;
    color(enum_wrapper rhs) : enum_wrapper(rhs) {}

    static constexpr wrapped_type red{LIB_COLOR_RED};
    static constexpr wrapped_type green{LIB_COLOR_GREEN};
    static constexpr wrapped_type blue{LIB_COLOR_BLUE};
  };

  // <insert copy of lib_status documentation>
  struct status : detail::enum_wrapper<lib_status> {
    using enum_wrapper::enum_wrapper;
    status(enum_wrapper rhs) : enum_wrapper(rhs) {}

    static constexpr wrapped_type ok{LIB_STATUS_OK};
    static constexpr wrapped_type {LIB_STATUS_BAD_ARGUMENT};
  };

  inline status foo(color in_col) {
    return lib_foo(in_col);
  }
}

Using a wrapped enum:

static_assert(std::is_trivial_v<lib::status>);

int main() {
  lib::status success = lib::status::ok;
  success = LIB_STATUS_OK;
  success = lib::foo(lib::color::red);
  success = lib_foo(LIB_COLOR_RED);

  // if(success == LIB_COLOR_RED) {} // DOES NOT COMPILE

  // can switch
  switch(success) {
    case lib::status::ok: break;
    case lib::status::bad_argument: break;
  }

  // Missing cases are caught: DOES NOT COMPILE
  // switch(success) {
  //  case lib::status::ok: break;
  // }

  // These might look undesirably loose at a glance. 
  // However, this allows using the c++ wrapper with 3rd-party
  // libraries that only implements a C api. e.g. using glfw() 
  // alongside vulkan.hpp
  lib_foo(lib::color::green); // Can call C api with wrapper
  lib::foo(LIB_COLOR_RED); // Can still use the C enum in the C++ api
  // lib::foo(LIB_STATUS_OK); // DOES NOT COMPILE
}

See it all on godbolt: https://gcc.godbolt.org/z/5d6cTPPnb

I'm looking for general feedback and/or potential pitfalls users of the wrapped library could run into.

\$\endgroup\$
5
  • 4
    \$\begingroup\$ were you aware of the site policy that you are absolutely not, for any reason, supposed to edit the code in a post after it has been answered? Changes made after an answer may cause the answers to appear invalid. \$\endgroup\$ Commented Jun 7, 2021 at 15:30
  • \$\begingroup\$ @Donald.McLean I was not, and I apologize. The only edits I have applied are bringing it in line with the policy that it has to compile, and clarifying intent. But I won't even do that in the future. Thank you very much for the callout. I'm not super familiar with this stackexchange yet. \$\endgroup\$
    – user128454
    Commented Jun 7, 2021 at 15:32
  • \$\begingroup\$ @Donald I have also rollbacked the changes. Thank you again. \$\endgroup\$
    – user128454
    Commented Jun 7, 2021 at 15:33
  • 2
    \$\begingroup\$ Not a problem. And actually, you are correct that the code should compile, which is probably the only exception to the rule. If you can make a change that allows the code to compile WITHOUT invalidating anything said in one or more of the posted answers, then you're OK. Normally, people wait until the code is good before answering. \$\endgroup\$ Commented Jun 7, 2021 at 15:40
  • \$\begingroup\$ @Donald.McLean Aliright, unrollebacked. As mentionned in the comment, the code causing a compilation error was actually part of the demonstration that the feature is working as expected, so it's all a bit confusing \$\endgroup\$
    – user128454
    Commented Jun 7, 2021 at 15:44

1 Answer 1

1
\$\begingroup\$

typedef enum lib_status lib_status;
You don't need that in a C++ header; the enumeration tag is already a stand-alone type name in C++.

There is an obligatory commented needed grumbling about the need to define all the relational operators individually due to the need to be compatible with C++(<20). Likewise for defining operator!= after having defined ==.

Check out a library such as Catch2 for writing the unit tests.

static_assert(std::is_trivial_v<lib::status>);
Nice to see you including this... you ought to make it part of the class template, as it should be the case for every use. It's not just for testing.

The only real safely in your wrapper, from what can tell, is that different strong types are incompatible. So, you should try passing the wrong wrapped type for a parameter (where the argument type is a different strong type, and when the argument type is the wrong C enum type) causes a compile-time error.

update: The original code was very confusing that it contained lines mixed throughout that would not compile, with no indication that they won't. My above paragraph stands to point out that there are ways to include positive "this won't compile" checks in runnable unit tests.

\$\endgroup\$
13
  • \$\begingroup\$ "You don't need that in a C++ header" This library is explicitely meant as a wrapper around a C library, so it made sense to me that the usage example uses pure C syntax. \$\endgroup\$
    – user128454
    Commented Jun 7, 2021 at 15:08
  • \$\begingroup\$ "you should try passing the wrong wrapped type" I think I'm misreading this, isn't that what lib::foo(LIB_STATUS_OK); does, correctly failing to compile? Obviously, I can't possibly do anything about lib_foo(LIB_STATUS_OK). \$\endgroup\$
    – user128454
    Commented Jun 7, 2021 at 15:10
  • 1
    \$\begingroup\$ you ought to make it part of the class template Woops, copy-paste error. My actual code has an even stricter static_assert(std::is_enum_v<EnumT>) as part of the wrapper type. Good eye! \$\endgroup\$
    – user128454
    Commented Jun 7, 2021 at 15:12
  • \$\begingroup\$ "Check out a library such as Catch2 for writing the unit tests." Good advice! My actual library does it with doctest instead, I just wanted a godbolt-friendly version online. \$\endgroup\$
    – user128454
    Commented Jun 7, 2021 at 15:14
  • 2
    \$\begingroup\$ Oh, I see what you mean: you emulated including the C header here. \$\endgroup\$
    – JDługosz
    Commented Jun 7, 2021 at 15:19

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service and acknowledge you have read our privacy policy.