У меня есть следующий код, в котором меня раздражает тот факт, что компилятор не может увидеть, что переменная, переданная в качестве аргумента функции, - 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 {
Невозможно воспроизвести с последними версиями gcc и clang.
Код @evg, который я вставил, работает, если вы измените его, чтобы get_evens принимает массив в качестве аргумента, он не работает
Затем добавьте код, который не работает. Сам аргумент функции никогда не будет constexpr.
Ваш пример далек от минимальный воспроизводимый пример ...
Я отредактировал ваш код, проверьте, правильно ли я истолковал ваше намерение.
@Evg Меня устраивает, что arr является constexpr static, но в остальном да.
Что такое yi3ld?
@ Yakk-AdamNevraumont неправильно написал yield, так как я надеюсь, что WG21 увидит свет и удалит co _... Почему он используется: чтобы та же функция могла использоваться для определения размера результирующего массива и его заполнения.





Аргумент функции никогда не является постоянным выражением, даже если функция используется в контексте 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:
constexprfunctions can be used in contexts that demand compile-time constants. If the values of the arguments you pass to aconstexprfunction 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
constexprfunction 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. Theconstexprfunction does it all.
Я надеюсь, что кто-то придумает более удачный прием, но я в этом сомневаюсь ... Так что я, вероятно, приму ваш ответ в конце. :)
@NoSenseEtAl, взгляни на std::integer_sequence. Возможно, вы можете использовать его здесь вместо std::array.
Проблема с тем, чтобы сделать arr параметром шаблона, заключается в том, что это не будет работать для локальных массивов constexpr.
@Holt, я не предлагаю делать сам arr параметром шаблона, нужен какой-то обходной путь. Я просто продемонстрировал принцип.
@Evg Тогда вам нужно расширить свой ответ. Как бы то ни было, вы просто указываете на проблему и даете ответ на очень упрощенную версию исходной проблемы, которую трудно распространить на реальную проблему.
@Evg: Могу ли я отредактировать ваш ответ, чтобы добавить текст из книги Скотта Мейера Modern C++? Думаю, он это тоже объясняет.
@ConstantinosGlynos, может быть, лучше дать отдельный ответ. Трудно понять, не зная, что вы хотите написать
@NoSenseEtAl: Я собирался, но не хотел повторять то, что уже сказал Евгений. Это почти тот же код, и мне нравится его упрощенный пример. Я просто почувствовал, что для ответа можно использовать немного больше резервной копии из книжной ссылки.
@Evg: Не беспокойтесь! :-) Вы видели цитаты из книги?
@ConstantinosGlynos, ваша правка была отклонена автоматически из-за моей правки, поэтому я добавил ее сам. Спасибо!
«Если значения аргументов, которые вы передаете функции constexpr в таком контексте, известны во время компиляции, результат будет вычислен во время компиляции». прямо противоположная ситуация в этом вопросе. : D
@NoSenseEtAl, не совсем так. Вы можете передать std::array в качестве аргумента и получить результат constexpr, но внутри функции этот массив не является самим constexpr. Я бы сказал, что эта цитата не объясняет источник проблемы.
@NoSenseEtAl Многие люди заблуждаются относительно функций constexpr. Если функция F - это constexpr, F(x) можно оценить во время компиляции, если x уже является константным выражением, но F(x) будет оцениваться во время выполнения, если x доступен только во время выполнения, поэтому в F вы не можете рассматривать x как постоянное выражение. Только результат F(x) можно считать постоянным выражением.
Другой ответ имеет правильную работу, но я думаю, что рассуждения не имеют ничего общего с параметрами, а вместо этого связаны с захватом лямбда здесь:
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. Цитата из книги Скоттса Мейера действительно не очень актуальна для самого ответа.
Проблема не в лямбде, проблема в использовании cnt как постоянного выражения вget_evens(). Все ваши примеры не будут компилироваться, если вы попытаетесь использовать результат выражения внутри самой функции. Почему? Потому что функция constexpr должна быть допустимой для constexpr, но также и для аргументов, отличных от constexpr. Проблема в коде OP заключается в том, что cnt является результатом вычислений с участием arr, то есть мы могли бы записать int cnt = f(arr);, независимо от того, является ли f (constexpr или нет). [...]
[...] Использование cnt в постоянном выражении означало бы, что мы можем вычислить f(arr) во время компиляции, что не может быть гарантировано, если arr является параметром, поскольку вы не можете заставить функции принимать только аргументы constexpr. Единственный способ сделать это - передать эти аргументы в качестве параметров шаблона, что может быть сделано в некоторых случаях, но имеет некоторые ограничения.
Следуя предложению 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 - Пункт 2: Ой! Не понимаю, как я сам об этом не подумал. Это более простое применение сворачивания C++ 17. Спасибо. Пункт 1: вы имеете в виду const & для аргументов get_evens()? Как думаете, почему лучше без const &?
std::integer_sequence - это пустой struct, его размер 1 байт, передавать по ссылке нет смысла. Оптимизатор все равно позаботится об этом, но это не так.
#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 НЕ
можете ли вы объяснить, как invoke_if обходит предел, упомянутый в других ответах (что arr не constexpr внутри тела функции)
еще одно примечание: clang 7 компилирует ваш код gcc.godbolt.org/z/r2xair
@nose хитрость в том, что код должен быть действительным как во время выполнения, так и во время компиляции. Хитрость в том, что я отделяю длину от значений.
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 правда, потому что я хотел вывести тип; Я мог бы сделать тип явным или что-то в этом роде и использовать автоматическое определение для фабричного шаблона. Вывод целочисленной последовательности позволяет использовать ее в других контекстах времени компиляции.