Can you use C++ concepts in the same way as polymorphic interfaces?

47 Views Asked by At

I have just stumbled over C++20's "concepts", something that I've wanted in the language for years... well kind of.

Consider the code below:

#include <concepts>
#include <iostream>

template <typename T>
concept IFoo = requires(T t) {
    { t.foo() } -> std::convertible_to<int>;
};

class FooImpl
{
    public:
        int foo() const { return 3; }
};

template <IFoo Foo>
void doSomething(const Foo& foo) {
    std::cout << foo.foo() << "\n";
    std::cout << foo.bar() << "\n"; // I want this to be an error!
}

int main() {
    FooImpl foo;
    doSomething(foo); // No error if this is commented out
    return 0;
}

I am trying to use the concept IFoo as you would use a normal polymorphic interface (= pure virtual class). The problem that I have is that the call foo.bar() does not produce a compile error, merely a substitution error. This means that the concept IFoo could lie to you, for example when coding like this:

  1. Write doSomething where you only call foo.foo() and create the concept for documentation
  2. Two weeks later: Extend doSomething to also call foo.bar() and forget to update the concept.
  3. Another two weeks later: Write a test for doSomething and be dumbfounded when you get a template error even though you completely satisfy the concept in your mock implementation of IFoo.

Right now, I don't see the advantage of using concepts over simple doc comments:

#include <iostream>

class FooImpl
{
    public:
        int foo() const { return 3; }
};

/**
 * Foo needs to contain the following functions:
 * 
 * - int Foo::foo()
 */
template <typename Foo>
void doSomething(const Foo& foo) {
    std::cout << foo.foo() << "\n";
    std::cout << foo.bar() << "\n"; // I want this to be an error!
}

int main() {
    FooImpl foo;
    doSomething(foo); // No error if this is commented out
    return 0;
}

Sure, the doc comment can rot, but so can the concept. Is there a way to produce a compile error if you use a function on a concept that is not contained in the concept?

1

There are 1 best solutions below

0
Jeff Garrett On

No.

What you want is true generics or "definition checking" as it was called in the C++ proposals. It was included in the C++0x (pre-C++11) concepts, but those were never accepted by the committee.

There are legitimately some C++ factors that make writing concepts more difficult if they were definition checked. For example, the concept in the post allows:

foo.foo() // #1

but not necessarily calling on a const or rvalue:

Foo(foo).foo() // there are types for which #1 works but this does not
static_cast<const Foo>(foo).foo() // same

That the result is convertible to an int means that you can use it where an int can, except you can't: if foo() directly returns an int, it can be implicitly converted to a type that is implicitly convertible from int. However, if foo() returns something convertible to int, it cannot be implicitly converted to this other type.

use(foo.foo()) // where use(const Bar&) and Bar is implicitly convertible from int
               // works if foo() returns int, but not convertible to int

So... as much as I and others would have preferred a "definition checked" version, to write a concept in such a C++ could be even trickier.

So... no, there is not a way to provide that compile error.

Concepts may have other advantages. For example, overloads will be ranked according to "subsumption": an overload taking a more specific concept will be chosen in place of one taking a more generic concept.