max max - 22 days ago 11
C++ Question

How to enforce a formal protocol with C++ templates?

When using the compile-time duck typing inherent with the template style, is there any way to enforce the requirement that the template argument implements certain methods with certain signatures?

struct ProtocolT {
void g() const;
void h();
}

// I want the compiler to check that T conforms to ProtocolT
// that is, T must implement g() and h() rather than just g()
template <typename T>
void f(const T& x) {
x.g();
}


Of course, even without this, there is perfect type safety: if the template argument
T
does not have a method used in the template function implementation, the compiler will always complain.

But I find it appealing to state clearly that
class T
must have all the methods specified in some
class ProtocolT
. It would allow me to constrain the design earlier in the development process by requiring methods from
T
that I don't yet use in the template function implementation.

Even if I didn't include any unused methods in
ProtocolT
, I still think a verified protocol conformance would help when I need to write a class usable as
T
. (Of course, no one stops me from writing
ProtocolT
for documentation purposes, but then the compiler won't validate that
ProtocolT
includes at least all the required methods.)

Answer Source

The feature you are looking for is known as concepts. They are currently a technical specification; GCC has an implementation of concepts lite.

Using concepts would look something like (I'm not too familiar with the syntax, so it would probably be slightly different):

template <typename T>
concept bool Protocol = requires(const T a, T b) {
  { a.g() } -> void;
  { b.h() } -> void;
};

void f(const Protocol& x) {
  x.g();
}

However, if you want a solution you can use right now, you can emulate concepts with a variety of techniques.

You could write type-traits to detect if a function does what you want.

You could also use the detection idiom, which abstracts the previous technique, greatly reducing boilerplate. For your example:

template <typename T>
using g_t = decltype(std::declval<const T&>().g());

template <typename T>
using h_t = decltype(std::declval<T&>().h());

template <typename T>
constexpr bool meets_protocol_v = std::experimental::is_detected_exact_v<void, g_t, T>
            && std::experimental::is_detected_exact_v<void, h_t, T>;

In using it, you could either be SFINAE friendly and SFINAE off of meets_protocol_v, or you could static assert:

template <typename T>
void f(const T& x) {
  static_assert(meets_protocol_v<T>, "Doesn't meet protocol");
  x.g();
}