Ensure class methods with C++20 concepts

Viewed 993

I love the feature of Interfaces in Java, and was looking forward to the new C++20 standart, introducing concepts.

On a current project i will have multiple implementations for the same thing. The rest of the code should be unaffected by that and handel them all in a general "one fits all" way. Further, to help other people coding there own implementation of this exchangeable part, i would like to have a central place for the documentation, describing all needed parts.

I tried to get this working for some time now, but i keep one struggeling with the C++20 concepts. Since nothing really worked i discribe what i would like to have with a small example:

/* Should have a element type, like float, int, double, std::size_t,... */
template <typename Class>
concept HasElementType = requires {
    typename Class::Element;
};

/* Central place for the documentation: in the concept.
 * Since all relevant parts should be listed here, they can be documentated.
 */
template < typename Class, typename T>
concept HasFunctions = requires {
    Class::Class(int);    /* has constructor with int */
    T Class::field;       /* has field with name "field" of type T */
    int Class::foo(T);    /* has method foo, taking T, returning int */
    T Class::bar(int);    /* has method bar, taking int, returning T */
    void Class::foobar(); /* has method foobar, taking void, returnung void */
};

/* put both togetter */
template <typename Cls>
concept MyInterface = HasElementType<Cls> && HasFunctions<Cls,typename Cls::Element>;

The above concept MyInterface should than ensure, that calling the function below via my_function<MyObject>() should work properly for different implementations MyObject ∈ {Implementaion1, Implementaion2,...}.

/* Some example function */
template<MyInterface MyObejct>
void my_function(){
    using T = MyObejct::Element;
    T t = 5;
    MyObejct myObject(1);
    T field = myObject.field;
    int foo = myObject.foo(t);
    T bar   = myObject.bar(1);
    myObject.foobar();
}

I have 3 questions regarding this:

  1. Is it possible with concepts, to accomplish that?
  2. Is this in a somewhat clean look possible? Since it should increase the readability via accessible documentation, it would not be usefull if the code for the concept is barely readable.
  3. Are concepts in generall the right approche, or are there other/better ways to accomplish that?

Thanks, moro

2 Answers

You haved asked multiple things, so I answer one by one. First your "HasElement" concept.

Here you can see how it works:

#include <iostream>
#include <type_traits>

class AWithStaticElement{
public:
    static int Element;
};
int AWithStaticElement::Element = 12;

class AWithInstanceElement{
public:
    int Element;
};

class AWithElementType{
public:
    using Element = int;
};

class AWithoutElement{
};

template<typename T>
    requires std::is_member_pointer_v<decltype(&T::Element)>
void Foo(T t)
{
    std::cout << "Has instance Element " << t.Element << "\n";
}

template<typename T>
requires std::is_pointer_v<decltype(&T::Element)>
void Foo(T t) 
    
{
    std::cout << "Has static Element " << t.Element << "\n";
}

template<typename T>
requires requires (T t) { typename T::Element; }
void Foo(T t)
{
    std::cout << "Has Element type\n";
}

template<typename... T>
void Foo(T&&... t)
{
    std::cout << "Has no Element!\n";
}

int main()
{
    Foo(AWithStaticElement{});
    Foo(AWithInstanceElement{});
    Foo(AWithElementType{});
    Foo(AWithoutElement{});
}

With concepts you can basically give a set of requirements a name. If a concepts is long you don't need to repeat it all the time.

You have a plausible idea, but that's just not the syntax for requires-expressions. You want something like

template < typename Class >
concept HasFunctions = requires(Class c, Class::Element e) {
    Class(1);  // don't use a null pointer constant here!
    { c.field } -> std::same_as<decltype(e)>;
    { c.foo(e) } -> std::same_as<int>;
    { c.bar(1) } -> std::same_as<decltype(e)>;
    c.foobar();
};

Note that there's no need to test Class::Element separately: if that type doesn't exist, then the atomic constraint simply evaluates to false as desired.

This isn't quite as strict as your phrasing suggests; it's sufficient that the class be constructible from an int (possibly via implicit conversions, default arguments, constructor templates, etc.), for example, and it ignores the return type of foobar entirely. However, as is rapidly becoming common advice for constraint authors, why do you care if foobar returns something? If you expect it to be void, you're not going to do much with the return value anyway. It's generally superior to require the interface that you will use (e.g., that you will pass an int here and ignore a value there) rather than trying to describe the implementation of the type in question. Accordingly, you might consider relaxing the std::same_as as well, perhaps with std::convertible_to.

Related