Search code examples
c++c++20c++-concepts

C++ concepts and conversion issue


In the following example:

#include <concepts>
#include <cstdint>
#include <iostream>

using namespace std;

integral auto multiply(integral auto p_val_1, integral auto p_val_2) {
  return p_val_1 * p_val_2;
}

int main() {
  {
    float f{multiply(4, 3)};
    cout << "f = " << f << endl;
  }

  {
    float f(multiply(4, 3));
    cout << "f = " << f << endl;
  }

  {
    constexpr uint16_t i1{4};
    constexpr uint16_t i2{3};
    float f{multiply(i1, i2)};
    cout << "f = " << f << endl;
  }

  {
    constexpr int i1{4};
    constexpr int i2{3};
    float f(multiply(i1, i2));
    cout << "f = " << f << endl;
  }

  return 0;
}

gcc (Ubuntu 11.3.0-1ubuntu1~22.04) 11.3.0, in float f{multiply(4, 3)};, reports

main.cpp:13:13: Non-constant-expression cannot be narrowed from type 'int' to 'float' in initializer list (fix available)
insert an explicit cast to silence this issue

and in float f{multiply(i1, i2)};, it reports

main.cpp:25:13: Non-constant-expression cannot be narrowed from type 'int' to 'float' in initializer list (fix available)
insert an explicit cast to silence this issue

In my (still) poor understanding of concepts, it should not be possible to assign the result of multiply to an object that would not satisfy the std::integral requirement, but using float f(multiply(i1, i2)); or float f(multiply(4, 3)); it is.

Would anyone be kind to explain why?


Solution

  • The only effect of putting a concept constraint on auto in the return type is that the program will fail to compile if auto is deduced to something that doesn't satisfy the concept. It has no other effect besides that.

    In your example multiply(4, 3) deduces the parameter types of multiply both to int and so p_val_1 * p_val_2 is also of type int and the return type is deduced to int. int satisfies std::integral, so the concept check is satisfied.

    That you are then converting the int result of multiply(4, 3) to something else is completely unaffected by the concept constraint on the return type or the function at all.


    The conversion is ill-formed with braces because initialization with braces doesn't allow narrowing conversions, which integral-to-floating-point conversions always are, except if the source expression is a constant expression with a value that can be represented exactly in the target type. In none of your cases is it a constant expression, because multiply is not marked constexpr or consteval.

    Aside from that any integral type is however implicitly convertible to any floating-point type. So with any other initialization syntax the program is well-formed.

    All of this is completely independent of the constraint on the return type of multiply.