Перегрузка constexpr
Связанный: функция, возвращающая constexpr, не компилируется
Мне кажется, что constexpr ограничен в полезности в C++11 из-за невозможности определить две функции, которые в противном случае имели бы одну и ту же сигнатуру, но одна из которых была бы constexpr, а другая - не constexpr. Другими словами, было бы очень полезно, если бы у меня мог быть, например, конструктор constexpr std::string, который принимает только аргументы constexpr, и конструктор non-constexpr std::string для аргументов не constexpr. Другим примером может быть теоретически сложная функция, которую можно сделать более эффективной с помощью состояния. Вы не можете легко сделать это с помощью функции constexpr, поэтому у вас остается два варианта: иметь функцию constexpr, которая работает очень медленно, если вы передаете аргументы не-constexpr, или полностью отказываетесь от constexpr (или пишете две отдельные функции, но вы можете не знать, какую версию позвонить).
Поэтому мой вопрос таков:
Возможно ли для совместимой со стандартом реализации C++11 разрешить перегрузку функций на основе аргументов constexpr, или это потребует обновления стандарта? Если это не разрешено, было ли это намеренно запрещено?
@NicolBolas: скажем, у меня есть функция, которая отображает enum
к std::string
, Самый простой способ сделать это, предполагая, что мой enum
идет от 0
в n - 1
это создать массив размера n
заполнен результатом.
Я мог бы создать static constexpr char const * []
и построить std::string
по возвращении (оплачивая стоимость создания std::string
объект каждый раз, когда я вызываю функцию), или я могу создать static std::string const []
и вернуть значение, которое я смотрю, оплатив стоимость всех std::string
Конструкторы в первый раз я вызываю функцию. Похоже, что лучшим решением было бы создать std::string
в памяти во время компиляции (похоже на то, что делается сейчас с char const *
), но единственный способ сделать это - предупредить конструктор о том, что он имеет constexpr
аргументы.
Для примера, отличного от std::string
конструктор, я думаю, что довольно просто найти пример, где, если бы вы могли игнорировать требования constexpr
(и, таким образом, создать не constexpr
функция), вы можете создать более эффективную функцию. Возьмем вопрос: constexpr вопрос, почему эти две разные программы работают с g++ в такое разное время?
Если я позвоню fib
с constexpr
аргумент, я не могу сделать лучше, чем компилятор, полностью оптимизирующий вызов функции. Но если я позвоню fib
с не constexpr
аргумент, я могу захотеть, чтобы он вызывал мою собственную версию, которая реализует такие вещи, как запоминание (что потребовало бы состояния), поэтому я получаю время выполнения, подобное тому, которое было бы моим временем компиляции, если бы я передал constexpr
аргумент.
9 ответов
Это должно быть перегружено в зависимости от результата constexpr
или нет, а не аргументы.
const std::string
может хранить указатель на литерал, зная, что он никогда не будет записан (используя const_cast
удалять const
от std::string
было бы необходимо, и это уже неопределенное поведение). Было бы просто необходимо сохранить логический флаг, чтобы запретить освобождение буфера во время уничтожения.
Но неconst
строка, даже если инициализирована из constexpr
аргументы, требует динамического выделения, потому что требуется записываемая копия аргумента, и, следовательно, гипотетическая constexpr
конструктор не должен использоваться.
Из стандарта (раздел 7.1.6.1 [dcl.type.cv]
), изменяя любой объект, который был создан const
является неопределенным поведением:
За исключением того, что любой член класса, объявленный mutable (7.1.1), может быть изменен, любая попытка изменить объект const в течение его времени жизни (3.8) приводит к неопределенному поведению.
Я согласен, что эта функция отсутствует - она мне тоже нужна. Пример:
double pow(double x, int n) {
// calculate x to the power of n
return ...
}
static inline double pow (double x, constexpr int n) {
// a faster implementation is possible when n is a compile time constant
return ...
}
double myfunction (double a, int b) {
double x, y;
x = pow(a, b); // call version 1 unless b becomes a compile time constant by inlining
y = pow(a, 5), // call version 2
return x + y;
}
Теперь я должен сделать это с помощью шаблонов:
template <int n>
static inline double pow (double x) {
// fast implementation of x ^ n, with n a compile time constant
return ...
}
Это хорошо, но я упускаю возможность перегрузки. Если я делаю библиотечную функцию для использования другими, то неудобно, что пользователь должен использовать разные вызовы функций в зависимости от того, является ли n постоянной времени компиляции, и может быть трудно предсказать, уменьшил ли компилятор n до постоянная времени компиляции или нет.
Обнаружения constexpr
не может быть сделано с использованием перегрузок (как уже отвечали другие), но перегрузки являются лишь одним из способов сделать это.
Типичная проблема заключается в том, что мы не можем использовать то, что может улучшить производительность во время выполнения (например, для вызоваconstexpr
функции или для кэширования результатов) в constexpr
функция. Таким образом, мы можем получить два разных алгоритма, один менее эффективный, но записываемый как constexpr
другие оптимизированы для быстрого запуска, но не constexpr
, Тогда мы хотим, чтобы компилятор не выбирал constexpr
алгоритм для значений времени выполнения и наоборот.
Это может быть достигнуто путем обнаружения constexpr
и выбирая на его основе "вручную", а затем сокращая интерфейс до макроса препроцессора.
Сначала давайте иметь две функции. В общем случае функции должны достигать одинакового результата с разными алгоритмами. Я выбираю два алгоритма, которые никогда не дают одинаковых ответов, просто чтобы проверить и проиллюстрировать идею:
#include <iostream> // handy for test I/O
#include <type_traits> // handy for dealing with types
// run-time "foo" is always ultimate answer
int foo_runtime(int)
{
return 42;
}
// compile-time "foo" is factorial
constexpr int foo_compiletime(int num)
{
return num > 1 ? foo_compiletime(num - 1) * num : 1;
}
Тогда нам нужен способ обнаружить этот аргумент - компиляция выражения постоянной времени. Если мы не хотим использовать специфичные для компилятора способы, такие как __builtin_constant_p
тогда есть способы обнаружить это и в стандартном C++. Я почти уверен, что Йоханнес Шауб придумал следующий трюк, но не могу найти цитату. Очень хороший и понятный трюк.
template<typename T>
constexpr typename std::remove_reference<T>::type makeprval(T && t)
{
return t;
}
#define isprvalconstexpr(e) noexcept(makeprval(e))
noexcept
оператор должен работать во время компиляции, поэтому ветвление на его основе будет оптимизировано большинством компиляторов. Итак, теперь мы можем написать макрос "foo", который выбирает алгоритм на основе constexprness аргумента и проверяет его:
#define foo(X) (isprvalconstexpr(X)?foo_compiletime(X):foo_runtime(X))
int main(int argc, char *argv[])
{
int a = 1;
const int b = 2;
constexpr int c = 3;
const int d = argc;
std::cout << foo(a) << std::endl;
std::cout << foo(b) << std::endl;
std::cout << foo(c) << std::endl;
std::cout << foo(d) << std::endl;
}
Ожидаемый результат:
42
2
6
42
На тех немногих компиляторах, которые я пробовал, это работает как ожидалось.
Хотя в C++11 нет такой вещи, как "перегрузка constexpr", вы все равно можете использовать GCC/Clang __builtin_constant_p
внутренняя. Обратите внимание, что эта оптимизация не очень полезна для double pow(double)
потому что и GCC, и Clang уже могут оптимизировать pow для постоянных целочисленных показателей, но если вы напишите мультипрецизионную или векторную библиотеку, тогда эта оптимизация должна работать.
Проверьте этот пример:
#define my_pow(a, b) (__builtin_constant_p(b) ? optimized_pow(a, b) : generic_pow(a, b))
double generic_pow(double a, double b);
__attribute__((always_inline)) inline double optimized_pow(double a, double b) {
if (b == 0.0) return 1.0;
if (b == 1.0) return a;
if (b == 2.0) return a * a;
if (b == 3.0) return a * a * a;
if (b == 4.0) return a * a * a * a;
return generic_pow(a, b);
}
double test(double a, double b) {
double x = 2.0 + 2.0;
return my_pow(a, x) + my_pow(a, b);
}
В этом примере my_pow(a, x)
будет расширен до a*a*a*a
(благодаря устранению мертвого кода) и my_pow(a, b)
будет расширен до прямого generic_pow
Звоните без каких-либо предварительных проверок.
TL;DR: это возможно в C++20, с std::is_constant_evaluated
в <type_traits>
заголовок.
Я наткнулся на этот вопрос с приложением, которое упомянул в , имея в виду: я хотел использовать более быстрый алгоритм во время выполнения, но более медленный (и удобный) алгоритм во время компиляции.
Используя пример в user528720 Tiib : своем ответеответе
#include <iostream>
#include <type_traits>
constexpr int foo(int i) {
if (std::is_constant_evaluated()) {
// compile-time branch
return (i > 1) ? foo(i - 1) * i : 1;
} else {
// runtime branch
return 42;
}
}
int main(int argc, char* argv[]) {
int a = foo(1);
const int b = foo(2);
constexpr int c = foo(3);
const int d = foo(argc);
std::cout << a << std::endl;
std::cout << b << std::endl;
std::cout << c << std::endl;
std::cout << d << std::endl;
}
дает результат
2
2
6
42
Вот ссылка на программу на godbolt.
Обратите внимание, что это действительно допустимая функция, потому что, как сказано в стандарте (цитата скопирована из cppreference ):
Функция должна удовлетворять следующим требованиям:
- существует по крайней мере один набор значений аргументов, так что вызов функции может быть оцененным подвыражением основного выражения-константы (для конструкторов достаточно использования в инициализаторе константы).
Обратите внимание, однако, что эта программа:
#include <iostream>
#include <type_traits>
constexpr int foo(int i) {
if (std::is_constant_evaluated()) {
// compile-time branch
return i > 1 ? foo(i - 1) * i : 1;
} else {
// runtime branch
return 42;
}
}
int main(int argc, char *argv[]) {
int a = 1;
const int b = 2;
constexpr int c = 3;
const int d = argc;
std::cout << foo(a) << std::endl;
std::cout << foo(b) << std::endl;
std::cout << foo(c) << std::endl;
std::cout << foo(d) << std::endl;
}
дает результат
42
42
42
42
Я не совсем уверен, почему это происходит, но я подозреваю, что это потому, что
std::cout
х
operator<<
не отмечен
constexpr
, поэтому все вызовы функций
foo
происходят во время выполнения. Интересно, однако, что в выходных данных сборки Godbolt (для x86-64 GCC 11.2) мы можем увидеть
42
встроенный. Таким образом, функция действительно оценивается во время компиляции, просто не так, как мы ожидали сначала.
Проблема, как заявлено, кажется неправильной.
std::string
По построению владеет памятью. Если вы хотите простую ссылку на существующий буфер, вы можете использовать что-то похожее на llvm::StringRef
:
class StringRef {
public:
constexpr StringRef(char const* d, size_t s): data(d), size(s) {}
private:
char const* data;
size_t size;
};
Конечно, есть облом, который strlen
и все остальные функции C не являютсяconstexpr
, Это похоже на недостаток Стандарта (подумайте обо всех математических функциях...).
Что касается состояния, вы можете (немного), если вы понимаете, как его хранить. Помните, что циклы эквивалентны рекурсиям? Ну, аналогично, вы можете "сохранить" состояние, передав его в качестве аргумента вспомогательной функции.
// potentially unsafe (non-limited)
constexpr int length(char const* c) {
return *c == '\0' ? 0 : 1 + length(c+1);
}
// OR a safer version
constexpr int length_helper(char const* c, unsigned limit) {
return *c == '\0' or limit <= 0 ? 0 : 1 + length_helper(c+1, limit-1);
}
constexpr int length256(char const* c) { return length_helper(c, 256); }
Конечно, эта форма этого состояния несколько ограничена (вы не можете использовать сложные конструкции), и это ограничение constexpr
, Но это уже огромный скачок вперед. Идти дальше означало бы углубление в чистоту (что вряд ли возможно в C++).
Возможно ли для совместимой со стандартом реализации C++11 разрешить перегрузку функций на основе аргументов constexpr, или это потребует обновления стандарта? Если это не разрешено, было ли это намеренно запрещено?
Если в стандарте не сказано, что вы можете что-то сделать, то позволить кому-то это сделать будет нестандартным поведением. И, следовательно, компилятор, который позволил это, будет реализовывать расширение языка.
В конце концов, это не обязательно плохо. Но это не будет соответствовать C++11.
Мы можем только догадываться о намерениях комитета по стандартам. Возможно, они сознательно не допустили этого, или это могло быть чем-то вроде надзора. Дело в том, что стандарт не допускает перегрузки, поэтому это не так.
Еще один вариант обнаружения компиляции во время компиляции с использованием SFINAE: http://coliru.stacked-crooked.com/a/f3a2c11bcccdb5bf
template<typename T>
auto f(const T&)
{
return 1;
}
constexpr auto f(int)
{
return 2;
}
////////////////////////////////////////////////////////////////////////
template<typename T, int=f(T{})>
constexpr bool is_f_constexpr_for(int) {return true;}
template<typename...>
constexpr bool is_f_constexpr_for(...) {return false;}
template<typename T>
auto g(const T& t)
{
if constexpr (is_f_constexpr_for<T>(0))
{
}
else
{
}
}
Можно определить, является ли данная статическая переменная хранения постоянным выражением, используя подход, предложенный Ричардом Смитом, основанный на сужающих правилах преобразования.
Мы можем назначить unsigned int
а consexpr
неотрицательный int
без сужения:
unsigned int u {std::max(0, -3)}; // compiles, max is constexpr
Однако мы не сможем сделать это, если будем использовать переменную:
int a = 3;
unsigned int u {std::max(0, a)}; // compilation error, narrowing int to unsigned int
Чтобы определить, int reference
является константным выражением, мы можем проверить, можно ли его присвоить unsigned int
без сужения положительным или отрицательным значением. Это должно быть возможно для любогоint
значение которого известно во время компиляции, т.е. может рассматриваться как постоянное выражение.
template<const int& p> std::true_type
is_constexpr_impl(decltype((unsigned int){std::max(-p, p)}));
template<const int& p> std::false_type
is_constexpr_impl(...);
template<const int& p> using is_constexpr =
decltype(is_constexpr_impl<p>(0));
Теперь у нас могут быть разные реализации для времени выполнения и времени компиляции с использованием макроса:
int foo_runtime(int num) {
return num;
}
constexpr int foo_compiletime(int num) {
return num + 1;
}
#define foo(X) (is_constexpr<X>()?foo_compiletime(X):foo_runtime(X))
И, как уже говорилось, он имитирует перегрузку для выражения const:
int main() {
static int a = 3;
static const int b = 42; // considered constexpr
static const int c = foo_runtime(42); // not constexpr
static constexpr int d = 4;
static constexpr int e = -2;
static int f = 0;
static const int g = 0; // considered constexpr
std::cout << foo(a) << std::endl;
std::cout << foo(b) << std::endl;
std::cout << foo(c) << std::endl;
std::cout << foo(d) << std::endl;
std::cout << foo(e) << std::endl;
std::cout << foo(f) << std::endl;
std::cout << foo(g) << std::endl;
}
Это хорошо, но не очень полезно, поскольку ограничивается статическими переменными хранилища. Но есть перегрузка на основеconstexpr
.
Другой подход к достижению того же самого, вне зависимости от сужения преобразования, может заключаться в следующем:
template<const int& p> std::true_type
is_constexpr_impl(std::array<int, std::max(p, -p)>);
template<const int& p> std::false_type
is_constexpr_impl(...);
template<const int& p> using is_constexpr =
decltype(is_constexpr_impl<p>(0));
Использование std::array
выше заменяет использование простого c-массива, который не подходит для gcc с этим подходом.
Или другой - опять же, не полагаясь на правила сужения - который тоже отлично работает:
template<const int& p, typename T = void>
struct is_constexpr: std::false_type {};
template<const int& p>
struct is_constexpr<p, std::void_t<int[std::max(p,-p)+1]>>: std::true_type {};
Обратите внимание, что если мы попытаемся добиться того же с помощью более простого подхода:
template<typename T>
struct is_constexpr: std::false_type {};
template<typename T>
struct is_constexpr<const T>: std::true_type {};
#define foo(X) (is_constexpr<decltype(X)>()?foo_compiletime(X):foo_runtime(X))
Мы не достигли бы нашей цели по этой строке:
static const int c = foo_runtime(42); // const but not constexpr