最新消息:雨落星辰是一个专注网站SEO优化、网站SEO诊断、搜索引擎研究、网络营销推广、网站策划运营及站长类的自媒体原创博客

c++ - What does the To[1] mean in the concept is_convertible_without_narrowing? - Stack Overflow

programmeradmin1浏览0评论

The following concept from C++20 - The Complete Guide (adapted from /p0870), forbids narrowing conversions. E.g., float to int, as in 1.9f1.

template <typename From, typename To>
concept ConvertibleWithoutNarrowing = requires (From&& from) {
  { std::type_identity_t<To[]>{std::forward<From>(from)}} -> std::same_as<To[1]>;
};

The book uses this for a collection C that must not have narrowing conversions when adding data:

template<typename C, typename T>
requires ConvertsWithoutNarrowing<T, typename C::value_type>
void add(C& collection, const T& val) {…}

// Usage:
std::vector<int> vec_i;
add(vec_i, 1); // OK
add(vec_i, 1.3); // Does not compile.

I get the general idea behind the concept, but what does the [1] in the last part, std::same_as<To[1]>;, do?

The following concept from C++20 - The Complete Guide (adapted from http://wg21.link/p0870), forbids narrowing conversions. E.g., float to int, as in 1.9f1.

template <typename From, typename To>
concept ConvertibleWithoutNarrowing = requires (From&& from) {
  { std::type_identity_t<To[]>{std::forward<From>(from)}} -> std::same_as<To[1]>;
};

The book uses this for a collection C that must not have narrowing conversions when adding data:

template<typename C, typename T>
requires ConvertsWithoutNarrowing<T, typename C::value_type>
void add(C& collection, const T& val) {…}

// Usage:
std::vector<int> vec_i;
add(vec_i, 1); // OK
add(vec_i, 1.3); // Does not compile.

I get the general idea behind the concept, but what does the [1] in the last part, std::same_as<To[1]>;, do?

Share Improve this question edited Jan 20 at 11:27 CommunityBot 11 silver badge asked Jan 19 at 19:36 JohanJohan 76.8k27 gold badges200 silver badges337 bronze badges 2
  • 4 Does the book really not add any explanation for why this concept looks the way it does? – Barry Commented Jan 19 at 19:44
  • 2 @Barry, no the book does not, it just says that the concept is "a short tricky requirement". Nor does P0870, it just says it uses a workaround to declare T as an array, but I fail to understand how referring to the second element of some workaround array relates to conversions between int and float. – Johan Commented Jan 19 at 19:53
Add a comment  | 

1 Answer 1

Reset to default 26

TL;DR the [1] explicitly specifies that the size of the array being used for comparison has to be, well, 1. And using an array saves a ton of work.


Further explanation:

1. But why use an array in the first place?

C++ constrains on array type narrowing

When creating an array, C++ requires that the type of elements used to initialize the array must not involve any narrowing conversions

So if you try to compile

int arr[1] = {1.3};

You'll get an error along the lines of narrowing conversion from double to int. Initialization fails because you cannot directly assign a double to an int array without explicitly truncating or converting the value.

    int x = 10.5; // WARNING: Implicit conversion from 'double' to 'int' changes value from 10.5 to 10
    int y[1] = {10.5}; // ERROR: Type 'double' cannot be narrowed to 'int' in initializer list

2. About that "short tricky requirement"

The provided concept

{ std::type_identity_t<To[]>{std::forward<From>(from)}} -> std::same_as<To[1]>;

Ensures that a value of type From can be converted to type To without losing information (i.e., no narrowing conversions are allowed). The trick lies in leveraging the stricter rules arround array initialization and narrowing conversions as to avoid the need to explicitly write checks for those cases (e.g., integral-to-floating-point, floating-point-to-integral).

Without it you'd need a lot more involvement to achive a similar result: that concept would explode out to:

template <typename From, typename To>
concept ConvertibleWithoutNarrowing = requires(From&& from) {
        { static_cast<To>(std::forward<From>(from)) } -> std::convertible_to<To>;
    } &&
    []() constexpr {
        if constexpr (std::is_integral_v<From> && std::is_integral_v<To>) {
            return sizeof(From) <= sizeof(To);
        } else if constexpr (std::is_floating_point_v<From> && std::is_integral_v<To>) {
            return false;
        } else {
            return true;
        }
    }();

And that only covers the case of aritmethic types! for a bonafide equivalent implementation with out relying on array initialization constraints you'd need to add each possible narrowing case again, by hand.


3. Another way to think about concepts: they ask questions about types

A simple way of getting arround to what using ConvertibleWithoutNarrowing does, is to think of it as "asking" the following question:

To array[1] = {From}; // Is <- that valid C++?

The answer is "yes" if From can be converted to To whitout narrowing.

For comparison, a possible implementation of a concept that checks conversion that allows narrowing:

template <typename From, typename To>
concept ConvertibleWithNarrowing = requires (From&& from) {
    { static_cast<To>(std::forward<From>(from)) } -> std::same_as<To>;
};

You could think of this concept as "asking":

To x = From; // Is <- that valid C++?

Now consider those "questions" for the case that From = double and To = int:

  1. Checking for ConvertibleWithoutNarrowing fails because type double cannot be narrowed to int in initializer list; but
  2. Checking for ConvertibleWithNarrowing passes, as narrowing double to int outside a list initialization is allowed - it just truncates it to an integer.
发布评论

评论列表(0)

  1. 暂无评论