C++ «забывает», что переменная constexpr при использовании в качестве аргумента функции

У меня есть следующий код, в котором меня раздражает тот факт, что компилятор не может увидеть, что переменная, переданная в качестве аргумента функции, - constexpr, поэтому я должен использовать функцию arity 0 вместо функции с одним аргументом.

Я знаю, что это не ошибка компилятора, но мне интересно, есть ли идиомы, которые позволяют обойти эту проблему.

#include <array>
#include <iostream>

static constexpr std::array<int, 5> arr{11, 22, 33, 44, 55};

template <typename C, typename P, typename Y>
static constexpr void copy_if (const C& rng, P p, Y yi3ld) {
    for (const auto& elem: rng) {
        if (p(elem)){
            yi3ld(elem);
        }
    }
}

// template<std::size_t N>
static constexpr auto get_evens(/* const std::array<int, N>& arr */) {
    constexpr auto is_even = [](const int i) constexpr {return i % 2 == 0;};
    constexpr int cnt = [/* &arr, */&is_even]() constexpr {
        int cnt = 0;
        auto increment = [&cnt] (const auto&){cnt++;};
        copy_if (arr, is_even, increment);
        return cnt;
    }();
    std::array<int, cnt> result{};
    int idx = 0;
    copy_if (arr, is_even, [&result, &idx](const auto& val){ result[idx++] = val;});
    return result;
}

int main() {
    // constexpr std::array<int, 5> arr{11, 22, 33, 44, 55};
    for (const int i:get_evens(/* arr */)) {
        std::cout << i << " " << std::endl;
    }
}

Если неочевидно, что я хочу: я хотел бы изменить подпись get_evens, чтобы она была шаблоном для массива размером N и принимала 1 аргумент типа const std::array<int, N>&.

Сообщение об ошибке, когда я изменяю arr на аргумент функции, бесполезно:

prog.cc:25:21: note: initializer of 'cnt' is not a constant expression prog.cc:19:19: note: declared here constexpr int cnt = [&arr, &is_even]()constexpr {

«Я знаю, что это не ошибка компилятора» - не уверен.
StoryTeller - Unslander Monica 15.10.2018 10:05

Невозможно воспроизвести с последними версиями gcc и clang.

Evg 15.10.2018 10:07

Код @evg, который я вставил, работает, если вы измените его, чтобы get_evens принимает массив в качестве аргумента, он не работает

NoSenseEtAl 15.10.2018 10:08

Затем добавьте код, который не работает. Сам аргумент функции никогда не будет constexpr.

Evg 15.10.2018 10:09
wandbox.org/permlink/SB8Ka1voP0pCvhWR
NoSenseEtAl 15.10.2018 10:09

Ваш пример далек от минимальный воспроизводимый пример ...

YSC 15.10.2018 10:26

Я отредактировал ваш код, проверьте, правильно ли я истолковал ваше намерение.

Evg 15.10.2018 10:31

@Evg Меня устраивает, что arr является constexpr static, но в остальном да.

NoSenseEtAl 15.10.2018 10:32

Что такое yi3ld?

Yakk - Adam Nevraumont 15.10.2018 17:24

@ Yakk-AdamNevraumont неправильно написал yield, так как я надеюсь, что WG21 увидит свет и удалит co _... Почему он используется: чтобы та же функция могла использоваться для определения размера результирующего массива и его заполнения.

NoSenseEtAl 15.10.2018 20:00
Стоит ли изучать PHP в 2026-2027 годах?
Стоит ли изучать PHP в 2026-2027 годах?
Привет всем, сегодня я хочу высказать свои соображения по поводу вопроса, который я уже много раз получал в своем сообществе: "Стоит ли изучать PHP в...
Поведение ключевого слова "this" в стрелочной функции в сравнении с нормальной функцией
Поведение ключевого слова "this" в стрелочной функции в сравнении с нормальной функцией
В JavaScript одним из самых запутанных понятий является поведение ключевого слова "this" в стрелочной и обычной функциях.
Приемы CSS-макетирования - floats и Flexbox
Приемы CSS-макетирования - floats и Flexbox
Здравствуйте, друзья-студенты! Готовы совершенствовать свои навыки веб-дизайна? Сегодня в нашем путешествии мы рассмотрим приемы CSS-верстки - в...
Тестирование функциональных ngrx-эффектов в Angular 16 с помощью Jest
В системе управления состояниями ngrx, совместимой с Angular 16, появились функциональные эффекты. Это здорово и делает код определенно легче для...
Концепция локализации и ее применение в приложениях React ⚡️
Концепция локализации и ее применение в приложениях React ⚡️
Локализация - это процесс адаптации приложения к различным языкам и культурным требованиям. Это позволяет пользователям получить опыт, соответствующий...
Пользовательский скаляр GraphQL
Пользовательский скаляр GraphQL
Листовые узлы системы типов GraphQL называются скалярами. Достигнув скалярного типа, невозможно спуститься дальше по иерархии типов. Скалярный тип...
6
10
581
5
Перейти к ответу Данный вопрос помечен как решенный

Ответы 5

Аргумент функции никогда не является постоянным выражением, даже если функция используется в контексте constexpr:

constexpr int foo(int i)
{
    // i is not a constexpr
    return i + 1;
}

constexpr auto i = 1;
constexpr auto j = foo(i);    

Чтобы имитировать аргумент constexpr, используйте параметр шаблона:

template<int i>
constexpr int foo()
{
    // i is constexpr
    return i + 1;
}

constexpr auto i = 1;
constexpr auto j = foo<i>();

Возможное решение - использовать std::integer_sequence для кодирования целых чисел в тип:

#include <array>
#include <iostream>
#include <type_traits>

template<typename P, typename Y, int... elements>
constexpr void copy_if_impl(P p, Y yi3ld, std::integer_sequence<int, elements...>) {
    ((p(elements) && (yi3ld(elements), true)), ...);
}

template<typename arr_t, typename P, typename Y>
constexpr void copy_if (P p, Y yi3ld) {
    copy_if_impl(p, yi3ld, arr_t{});
}

template<typename arr_t>
constexpr auto get_evens(){
    constexpr auto is_even = [](const int i) constexpr { return i % 2 == 0; };
    constexpr int cnt = [&is_even]() constexpr {
        int cnt = 0;
        auto increment = [&cnt](const auto&) { cnt++; };
        copy_if<arr_t>(is_even, increment);
        return cnt;
    }();

    std::array<int, cnt> result{};
    int idx = 0;
    copy_if<arr_t>(is_even, [&result, &idx](const auto& val) {
        result[idx++] = val; });
    return result;
}

int main()
{
    using arr = std::integer_sequence<int, 11, 22, 33, 44, 55>;
    for (const int i : get_evens<arr>()) {
        std::cout << i << " " << std::endl;
    }
}

Дополнение предложено Константиносом Глиносом.

Из книги Эффективный современный C++ автора Скотт Мейерс, п.15, стр.98:

  • constexpr functions can be used in contexts that demand compile-time constants. If the values of the arguments you pass to a constexpr function in such a context are known during compilation, the result will be computed during compilation. If any of the arguments’ values is not known during compilation, your code will be rejected.
  • When a constexpr function is called with one or more values that are not known during compilation, it acts like a normal function, computing its result at runtime. This means you don’t need two functions to perform the same operation, one for compile-time constants and one for all other values. The constexpr function does it all.

Я надеюсь, что кто-то придумает более удачный прием, но я в этом сомневаюсь ... Так что я, вероятно, приму ваш ответ в конце. :)

NoSenseEtAl 15.10.2018 10:19

@NoSenseEtAl, взгляни на std::integer_sequence. Возможно, вы можете использовать его здесь вместо std::array.

Evg 15.10.2018 10:23

Проблема с тем, чтобы сделать arr параметром шаблона, заключается в том, что это не будет работать для локальных массивов constexpr.

Holt 15.10.2018 10:33

@Holt, я не предлагаю делать сам arr параметром шаблона, нужен какой-то обходной путь. Я просто продемонстрировал принцип.

Evg 15.10.2018 10:37

@Evg Тогда вам нужно расширить свой ответ. Как бы то ни было, вы просто указываете на проблему и даете ответ на очень упрощенную версию исходной проблемы, которую трудно распространить на реальную проблему.

Holt 15.10.2018 10:38

@Evg: Могу ли я отредактировать ваш ответ, чтобы добавить текст из книги Скотта Мейера Modern C++? Думаю, он это тоже объясняет.

Constantinos Glynos 15.10.2018 10:39

@ConstantinosGlynos, может быть, лучше дать отдельный ответ. Трудно понять, не зная, что вы хотите написать

NoSenseEtAl 15.10.2018 10:47

@NoSenseEtAl: Я собирался, но не хотел повторять то, что уже сказал Евгений. Это почти тот же код, и мне нравится его упрощенный пример. Я просто почувствовал, что для ответа можно использовать немного больше резервной копии из книжной ссылки.

Constantinos Glynos 15.10.2018 10:50

@Evg: Не беспокойтесь! :-) Вы видели цитаты из книги?

Constantinos Glynos 15.10.2018 10:55

@ConstantinosGlynos, ваша правка была отклонена автоматически из-за моей правки, поэтому я добавил ее сам. Спасибо!

Evg 15.10.2018 11:06

«Если значения аргументов, которые вы передаете функции constexpr в таком контексте, известны во время компиляции, результат будет вычислен во время компиляции». прямо противоположная ситуация в этом вопросе. : D

NoSenseEtAl 15.10.2018 11:07

@NoSenseEtAl, не совсем так. Вы можете передать std::array в качестве аргумента и получить результат constexpr, но внутри функции этот массив не является самим constexpr. Я бы сказал, что эта цитата не объясняет источник проблемы.

Evg 15.10.2018 11:14

@NoSenseEtAl Многие люди заблуждаются относительно функций constexpr. Если функция F - это constexpr, F(x) можно оценить во время компиляции, если x уже является константным выражением, но F(x) будет оцениваться во время выполнения, если x доступен только во время выполнения, поэтому в F вы не можете рассматривать x как постоянное выражение. Только результат F(x) можно считать постоянным выражением.

Holt 15.10.2018 17:14

Другой ответ имеет правильную работу, но я думаю, что рассуждения не имеют ничего общего с параметрами, а вместо этого связаны с захватом лямбда здесь:

constexpr int cnt = [/* &arr, */&is_even]() 

Действительно, с помощью этого кода мы можем протестировать различные сценарии:

#include <array> 
#include <iostream>

template <size_t N>
constexpr int foo(const std::array<int, N>& arr) {
    return [&arr] () { return arr.size(); }();
}

template <size_t N>
constexpr int bar(const std::array<int, N>& arr) {
    int res{};
    for (auto i : arr) {
        res++;
    }
    return res;
}

template <size_t N>
constexpr int baz(const std::array<int, N>& arr)     {
    constexpr int test = [&arr] () constexpr {
        return bar(arr);
    }();
    return test;
}

int main() {
    constexpr std::array<int, 5> arr{11, 22, 33, 44, 55};
    constexpr std::array<int, foo(arr)> test{};
    constexpr std::array<int, bar(arr)> test2{};
    constexpr std::array<int, baz(arr)> test3{};
}   

Обратите внимание, что строка, в которой инициализируется test3, не компилируется. Это, однако, отлично компилируется:

template <size_t N>
constexpr int baz(const std::array<int, N>& arr) {
    return bar(arr);
}

Итак, в чем проблема? На самом деле лямбды - это просто прославленные функторы, и внутренне это будет выглядеть примерно так:

struct constexpr_functor {
    const std::array<int, 5>& arr;
    constexpr constexpr_functor(const std::array<int, 5>& test)
        : arr(test) { }
    constexpr int operator()() const {
        return bar(arr);
    }
};
// ...
constexpr constexpr_functor t{arr};
constexpr std::array<int, t()> test3{};

Обратите внимание, что теперь мы получаем сообщение об ошибке, показывающее настоящую проблему:

test.cpp:36:33: note: reference to 'arr' is not a constant expression
test.cpp:33:34: note: declared here
    constexpr std::array<int, 5> arr{11, 22, 33, 44, 55};

Другой ответ цитирует книгу Скотта Мейера, но неверно истолковывает цитаты. В книге фактически показано несколько примеров параметров, используемых в ситуациях constexpr, но кавычки просто говорят о том, что если вы передадите параметр, не являющийся constexpr, функция может работать во время компиляции.

Я не понимаю, почему здесь важны лямбды. Что вы не можете сделать, так это это: constexpr void foo(int i) { std:array<int, i> arr; }. Но по сути это то, что делает OP, cnt играет роль i. Цитата из книги Скоттса Мейера действительно не очень актуальна для самого ответа.

Evg 15.10.2018 12:49

Проблема не в лямбде, проблема в использовании cnt как постоянного выражения вget_evens(). Все ваши примеры не будут компилироваться, если вы попытаетесь использовать результат выражения внутри самой функции. Почему? Потому что функция constexpr должна быть допустимой для constexpr, но также и для аргументов, отличных от constexpr. Проблема в коде OP заключается в том, что cnt является результатом вычислений с участием arr, то есть мы могли бы записать int cnt = f(arr);, независимо от того, является ли f (constexpr или нет). [...]

Holt 15.10.2018 14:20

[...] Использование cnt в постоянном выражении означало бы, что мы можем вычислить f(arr) во время компиляции, что не может быть гарантировано, если arr является параметром, поскольку вы не можете заставить функции принимать только аргументы constexpr. Единственный способ сделать это - передать эти аргументы в качестве параметров шаблона, что может быть сделано в некоторых случаях, но имеет некоторые ограничения.

Holt 15.10.2018 14:21

Следуя предложению Evg, передав числа как параметры шаблона std::integer_sequence, но передавая целочисленную последовательность как аргумент функции get_evens(), а не как параметр шаблона, вы можете использовать числа непосредственно внутри get_evens().

Я имею в виду ... вы можете упростить get_evens() следующим образом (РЕДАКТИРОВАТЬ: дальнейшее упрощение по предложению Evg (спасибо!))

template <typename T, T ... Ts>
constexpr auto get_evens (std::integer_sequence<T, Ts...> const &)
 {
   std::array<T, (std::size_t(!(Ts & T{1})) + ...)> result{};

   std::size_t idx = 0;

   ((void)(Ts & 1 || (result[idx++] = Ts, true)), ...);

   return result;
 } 

и вы можете использовать это так

int main()
 {
   using arr = std::integer_sequence<int, 11, 22, 33, 44, 55>;

   for ( const int i : get_evens(arr{}) )
      std::cout << i << " " << std::endl;
 }

Очень красивое упрощение. Мои предложения: 1) удалить const &, 2) constexpr std::size_t cnt = (!!(Ts & 1) + ...); или ((Ts % 2 == 0) + ...);.

Evg 15.10.2018 12:59

@Evg - Пункт 2: Ой! Не понимаю, как я сам об этом не подумал. Это более простое применение сворачивания C++ 17. Спасибо. Пункт 1: вы имеете в виду const & для аргументов get_evens()? Как думаете, почему лучше без const &?

max66 15.10.2018 13:25
std::integer_sequence - это пустой struct, его размер 1 байт, передавать по ссылке нет смысла. Оптимизатор все равно позаботится об этом, но это не так.
Evg 15.10.2018 13:33
Ответ принят как подходящий
#include <array>
#include <iostream>

static constexpr std::array<int, 5> arr{11, 22, 33, 44, 55};

template <typename C, typename P, typename T>
static constexpr void invoke_if (const C& rng, P p, T target) {
    for (const auto& elem: rng) {
        if (p(elem)){
            target(elem);
        }
    }
}

constexpr bool is_even(int i) {
    return i % 2 == 0;
}

template<std::size_t N>
constexpr std::size_t count_evens(const std::array<int, N>& arr)
{
    std::size_t cnt = 0;
    invoke_if (arr, is_even, [&cnt](auto&&){++cnt;});
    return cnt;
}

template<std::size_t cnt, std::size_t N>
static constexpr auto get_evens(const std::array<int, N>& arr) {
    std::array<int, cnt> result{};
    int idx = 0;
    invoke_if (arr, is_even, [&result, &idx](const auto& val){ result[idx++] = val;});
    return result;
}

int main() {
    // constexpr std::array<int, 5> arr{11, 22, 33, 44, 55};
    for (const int i:get_evens<count_evens(arr)>(arr)) {
        std::cout << i << " " << std::endl;
    }
}

это работает в g ​​++, но в clang мы получаем проблему, потому что begin на array неправильно constexpr с хотя бы одной библиотекой. Или, может быть, g ++ нарушает стандарт, а clang - нет.

nit: опечатка в конце: я полагаю, вы имеете в виду, что clang НЕ

NoSenseEtAl 15.10.2018 22:47

можете ли вы объяснить, как invoke_if обходит предел, упомянутый в других ответах (что arr не constexpr внутри тела функции)

NoSenseEtAl 15.10.2018 22:51

еще одно примечание: clang 7 компилирует ваш код gcc.godbolt.org/z/r2xair

NoSenseEtAl 15.10.2018 22:56

@nose хитрость в том, что код должен быть действительным как во время выполнения, так и во время компиляции. Хитрость в том, что я отделяю длину от значений.

Yakk - Adam Nevraumont 15.10.2018 23:33
template<auto t0, auto...ts>
struct ct_array:
  std::array<decltype(t0) const, 1+sizeof...(ts)>,
  std::integer_sequence<decltype(t0), t0, ts...>
{
  ct_array():std::array<decltype(t0) const, 1+sizeof...(ts)>{{t0, ts...}} {};
};

template<class target, auto X>
struct push;
template<auto X>
struct push<void, X>{using type=ct_array<X>;};
template<auto...elems, auto X>
struct push<ct_array<elems...>, X>{using type=ct_array<elems...,X>;};
template<class target, auto X>
using push_t= typename push<target, X>::type;

template<class target>
struct pop;
template<auto x>
struct pop<ct_array<x>>{using type=void;};
template<auto x0, auto...xs>
struct pop<ct_array<x0, xs...>>{using type=ct_array<xs...>;};
template<class target>
using pop_t=typename pop<target>::type;

template<class lhs, class rhs, class F, class=void>
struct transcribe;
template<class lhs, class rhs, class F>
using transcribe_t = typename transcribe<lhs, rhs, F>::type;

template<auto l0, auto...lhs, class rhs, class F>
struct transcribe<ct_array<l0, lhs...>, rhs, F,
  std::enable_if_t<F{}(l0) && sizeof...(lhs)>
>:
  transcribe<pop_t<ct_array<l0, lhs...>>, push_t<rhs, l0>, F>
{};
template<auto l0, auto...lhs, class rhs, class F>
struct transcribe<ct_array<l0, lhs...>, rhs, F,
  std::enable_if_t<!F{}(l0) && sizeof...(lhs)>
>:
  transcribe<pop_t<ct_array<l0, lhs...>>, rhs, F>
{};
template<auto lhs, class rhs, class F>
struct transcribe<ct_array<lhs>, rhs, F, void>
{
  using type=std::conditional_t< F{}(lhs), push_t<rhs, lhs>, rhs >;
};
template<class lhs, class F>
using filter_t = transcribe_t<lhs, void, F>;

// C++20
//auto is_even = [](auto i)->bool{ return !(i%2); };
struct is_even_t {
  template<class T>
  constexpr bool operator()(T i)const{ return !(i%2); }
};
constexpr is_even_t is_even{};

template<auto...is>
static constexpr auto get_evens(ct_array<is...>) {
  return filter_t< ct_array<is...>, decltype(is_even) >{};
}

Живой пример.

Код теста:

auto arr = ct_array<11, 22, 33, 44, 55>{};
for (const int i : get_evens(arr)) {
    std::cout << i << " " << std::endl;
}

Неужели в этом случае нужно, чтобы ct_array унаследовал от std::integer_sequence? И небольшая проблема, но этот код не компилируется в отфильтрованном массиве, пустом.

Holt 16.10.2018 10:36

@holt правда, потому что я хотел вывести тип; Я мог бы сделать тип явным или что-то в этом роде и использовать автоматическое определение для фабричного шаблона. Вывод целочисленной последовательности позволяет использовать ее в других контекстах времени компиляции.

Yakk - Adam Nevraumont 16.10.2018 12:58

Другие вопросы по теме