narrowing conversion в constexpr выражении

Есть функция, которая получает параметры через универсальные ссылки, затем инициализирует std::array этими параметрами

template<typename T, typename... Args>
concept BraceEnclosedInitializableFrom = requires(Args&&... args)
{
    T{std::forward<Args>(args)...};
};

template<typename T, typename... Args>
    requires BraceEnclosedInitializableFrom<std::array<T, 5>, Args...>
void foo(Args&&... args)
{
    [[maybe_unused]] std::array<T, 5> arr{std::forward<Args>(args)...};
}

При вызове функции:

void call()
{
    //foo<uint32_t>(1u, 2u, 3u, 4u, 5u); //работает
    //foo<std::string>("1","2","3","4","5"); //работает
    foo<uint32_t>(1, 2, 3, 4, 5); //не работает
}

При том, если закомментировать строку:

//requires BraceEnclosedInitializableFrom<std::array<T, 5>, Args...>

То вызов функции, триггерящий narrowing conversion, магическим образом начинает компилироваться (но только с GCC, clang выдаёт такую же ошибку).

Это можно починить, если немного переделать функцию, добавив static_cast

template<typename T, typename... Args>
//    requires BraceEnclosedInitializableFrom<std::array<T, 5>, Args...>
void foo(Args&&... args)
{
    [[maybe_unused]] std::array<T, 5> arr{static_cast<T>(std::forward<Args>(args))...};
}

Но это, как мне кажется, плохое решение, поскольку:

  • Это уже не perfect forwarding (Я привожу универсальную ссылку к нессылочному типу T
  • Непонятно, как переделать concept BraceEnclosedInitializableFrom

Пожалуйста, подскажите, как починить код, чтобы можно было:

  • Инициализировать std::array чем угодно, что в него передают (сейчас использую для этого perfect forwarding)
  • Работал концепт BraceEnclosedInitializableFrom (есть другие экземпляры функции foo(), которые иначе может быть невозможно вызвать)

Ответы (1 шт):

Автор решения: user754015

Помогло мне немного погуглить и подумать

В C++ ссылочный параметр функции не является constexpr выражением

void foo(const int& val) {
    constexpr int a = val; // error: ‘val’ is not a constant expression
}

void call() {
    foo(1);
}

Это важно, поскольку List-initialization разрешает narrowing conversion только в том случае, если значение является constexpr выражением + после конвертации значение помещается в приводимый тип.

Поскольку foo() принимает аргументы по (универсальной) ссылке

void foo(Args&&... args)

агрегатная инициализация массива, требующая конвертацию, просто не сработает.

std::array<T, 5> arr{std::forward<Args>(args)...};

Решить эту дилемму можно разными способами. Поскольку понятие narrowing conversion имеет смысл только в отношении арифметических типов, а perfect forwarding наоборот, имеет мало смысла при работе с ними, то можно сделать отдельный экземпляр foo(), принимающий исключительно числа, конвертирующий их в нужный тип и только затем инициализирующий массив.

Но, лучшее решение проблемы - самое простое:

void call() {
    foo<uint32_t>(1u, 2u, 3u, 4u, 5u); //работает, не трогай
}
→ Ссылка