I'm trying to use C++20 concepts to enforce an interface on multiple classes (and I do not want to use pure virtual functions). The problem I'm running into seems to be that implicit conversions allow things that should actually break.
I know about explicit
, but it can only be used with ctors and conversion functions.
Let me show some code to illustrate the problem:
#include <concepts>
#include <utility>
// =============================
// Define the interface
template<typename T>
concept ImplementsInterface = requires(T obj, int x)
{
{ obj.func(x) } -> std::same_as<int>;
};
// =============================
// Here is one implementation of the interface
struct B
{
double func(int x) // Note the INCORRECT 'double' return type
{
return x + 5;
}
};
// This DOES produce a compiler error (yay!)
static_assert(ImplementsInterface<B>);
// =============================
// Here is another implementation of the interface
struct C
{
int func(char x) // Note the INCORRECT 'char' argument
{
return x + 6;
}
};
// This DOES NOT produce a compiler error (and I want it to!)
static_assert(ImplementsInterface<C>);
(Godbolt: https://godbolt.org/z/4sbPhdGhK)
I think C::func(char)
uses implicit conversion from char
->int
and so the compiler does not flag it. How can I prevent that? I want an error in the static_assert
because of the incorrect argument type in C::func()
.
Thanks!
Concepts are based on expressions. An expression within a requires expression is valid if that expression can be evaluated and the evaluation results in a type that fulfills the optionally specified concept.
Implicit conversions are a function of expression evaluation. If an implicit conversion is permitted, then it is considered valid as far as the rules of C++ are concerned. So there isn't a simple way to arbitrarily say "don't do implicit conversions in this expression."
But for your cited specific case however, you can fix it:
// Define the interface
template<typename T>
concept ImplementsInterface = requires(T obj, int x)
{
{ obj.func({x}) } -> std::same_as<int>;
};
The use of {x}
forces the use of list initialization. Specifically copy-list-initialization. And copy-list-initialization will not allow "narrowing" implicit conversions. The conversion of an int
to a char
is known to be able to lose information (depending on the value of the int
), and this isn't allowed in copy-list-initialization.
But this does not work generally; it only works for known lossy conversions involving fundamental types.