max
max

Reputation: 52273

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.)

Upvotes: 4

Views: 808

Answers (2)

Justin
Justin

Reputation: 25317

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();
}

Upvotes: 4

user7860670
user7860670

Reputation: 37587

Maybe inserting corresponding static_assert:

static_assert
(
    ::std::is_same< void, decltype(::std::declval< T >().h()) >::value
,   "T must implement void h(void)"
);

Also note that in your example when T follows ProtocolT requirements it still won't work because f accepts a const reference to T, while ProtocolT only says it should have non-const g().

Upvotes: 2

Related Questions