c++c++20implicit-conversionc++-concepts

How can I prevent implicit conversion with C++ function arguments when using concepts?


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!


Solution

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