C++20 Modules as interfaces

Hey guys,
I'd like to use C++20 concepts to define interfaces. Usually I'd define interfaces using virtual functions but in this particular case I do not want any runtime overhead.
Concepts should generate some meaningful compiler errors

Let's consider this class

1
2
3
4
5
6
7
8
9
template<typename String_T, typename Layer_T>
class FooClass
{
public:
  String_T m_strFoo;
  void foo() {
       const String_T str = Layer_T::getId();
	}
};


a valid typename which can be passed as Layer_T could look like this
1
2
3
4
5
6
7
8
9
10
11
12
class SimpleIF_Impl
{
public:
  static std::string getName()
  {
    return "";
  }
  static void* getId()
  {
    return nullptr;
  }
};


creating an instance of foo class like this
FooClass<std::string, SimpleIF_Impl> f1;
would work.

Now I want to ensure via concepts that a correct IF is passed as a template parameter


When I'm creating a simple concept that does not rely on template parameters like this
1
2
3
4
5
template<typename T>
concept SimpleIF = requires (T a) {
  { T::getName() } -> std::same_as<std::string>; 
  { T::getId() } -> std::same_as<void*>; 
};


and adjusting my class to this
1
2
3
4
5
6
7
8
9
template<typename String_T, SimpleIF Layer_T>
class FooClass
{
public:
  String_T m_strFoo;
  void foo() {
  const std::string str = Layer_T::getId();
  }
};


I can create an instance of FooClass without any errors like this
FooClass<std::string, SimpleIF_Impl> f1;


Now what I haven't figured out is. How to deal with a concept which depends on a template parameter itself

1
2
3
4
5
6
template<typename T>
concept TemplatedIF = requires (T a) {
	{ T::getName() } -> std::same_as<std::string>;  
	typename T::task_type;
	{ T::getId() } -> std::same_as<typename T::task_type>; 
	};

after adjusting
1
2
template<typename String_T, TemplatedIF Layer_T>
class FooClass


I cannot create an instance of FooClass any more
writing
FooClass<std::string, SimpleIF_Impl> f1; gives me a compiler error

"template argument deduction failed for FooClass() String_T"
Tested with latest MSVC17


Could you help me out here? How can i handle templated concepts?

Last edited on
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
#include <iostream>
#include <concepts>
#include <string>

template < typename T >
concept interface = requires(T)
{
	typename T::name_type; // 1. has a nested type name_type (superfluous, subsumed by 2.)
	{ T::name() } -> std::same_as< typename T::name_type >; // 2. T::name() results in T::name_type
	{ T::name() } -> std::convertible_to<std::string>; // 3. T::name_type can be implicitly converted to std::string

	typename T::name_type::value_type ; // 4. nested type name_type has a nested type value_type (superfluous, subsumed by 5.)
	{ typename T::name_type::value_type() } -> std::same_as<char> ; // 5. value type of name_type is char (ergo it is default constructible)


	typename T::id_type; // 6. has a nested type id_type (superfluous, subsumed by 6.)
	{ T::id() } -> std::same_as< typename T::id_type >; // 7. T::id() results in T::id_type

	requires requires ( const typename T::name_type& n, const typename T::id_type& i, std::ostream& stm )
	{ stm << n << i ; } ; // 8. nested types name_type and id_type are output streamable types
};

template < interface IF > struct X
{
    X() { std::cout << "name: " << IF::name() << "   id: " << IF::id() << '\n' ; }
};

int main()
{
    struct A
    {
        using name_type = std::string ;
        static std::string name() noexcept { return "main::Y" ; }
        using id_type = int ;
        static id_type id() noexcept { return 999 ; }
    };

    X<A> xx ;
}

http://coliru.stacked-crooked.com/a/3c3e8fe6838e2964
wow JLBorges thx for this very detailed and nicely working answer :)

just a few understanding questions.


1) I can define custom types in a concept using typename T::myType right?

2) Why does A has to include the typenames via
using name_type = std::string ;
Why does
static std::string name() noexcept { return "main::Y" ; } alone not work?
> 1) I can define custom types in a concept using typename T::myType right?

No. We can only test for type validity.

Satisfaction of an atomic constraint is checked by substituting the parameter mapping and template arguments into the expression E. If the substitution results in an invalid type or expression, the constraint is not satisfied. Otherwise, E, after any lvalue-to-rvalue conversion, shall be a prvalue constant expression of type bool , and the constraint is satisfied if and only if it evaluates to true.
https://en.cppreference.com/w/cpp/language/constraints#Atomic%20constraints


> 2) Why does A has to include the typenames via using name_type = std::string ;

Because we have this contraint:
typename T::name_type; // 1. has a nested type name_type (line 8)
cool thank you. It's getting more clearer now.

I got 2 further questions.:)


1) Can I also check for functions with arguments?
I tried this but there seems to be a syntax error in may concept

1
2
3
4
5
 template < typename T >
 concept OsaLayer = requires(T a)
{
  { a.FunctionWithArguments(const T::name_type, int) } -> std::same_as<bool>;
}


2. Combine requirements
After watching some YouTube I have learning that different requirements can also be combined using common operators like ||.

Can I use the || within my single requirement as well?
{ T::name() } -> std::convertible_to<std::string> || { T::name().data() } -> std::convertible_to<std::string>; // 3. T::name_type can be implicitly converted to std::string

This has a syntax error as well


http://coliru.stacked-crooked.com/a/3ad87d2593c23b8a


Could you have a look at this :)
Thank for your great help so far !




> 1) Can I also check for functions with arguments?

May be something like this:
1
2
3
4
5
6
7
#include <concepts>

template < typename T >
concept OsaLayer = requires(T a)
{
  requires requires( typename T::name_type n ) { { a.FunctionWithArguments(n,100) } -> std::same_as<bool> ; } ;
};



2. Combine requirements

We can use helper concepts to simplify the disjunction. For instance:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
#include <concepts>
#include <string>

namespace helper
{
    template < typename T > concept has_stringifiable_name = requires( T a )
    { { T::name() } -> std::convertible_to<std::string>; } ;

    template < typename T > concept has_stringifiable_name_data = requires( T a )
    { { T::name().data() } -> std::convertible_to<std::string>; } ;
}

template < typename T > concept interface = requires(T a)
{
	requires helper::has_stringifiable_name<T> || helper::has_stringifiable_name_data<T> ;
};
cool thank your very much.
I'm getting a better understanding of concepts now :)
This works nicely :)
Topic archived. No new replies allowed.