Коли слід використовувати здатність constexpr в C ++ 11?


337

Мені здається, що мати "функцію, яка завжди повертає 5", це порушує або розбавляє значення "виклику функції". Повинно бути причина чи потреба в цій здатності, або це не було б в C ++ 11. Чому саме там?

// preprocessor.
#define MEANING_OF_LIFE 42

// constants:
const int MeaningOfLife = 42;

// constexpr-function:
constexpr int MeaningOfLife () { return 42; }

Мені здається, що якщо я написав функцію, яка повертає буквальне значення, і я підійшов до перегляду коду, хтось сказав би мені, я повинен тоді оголосити постійне значення замість того, щоб писати return 5.


28
Чи можете ви визначити рекурсивну функцію, яка повертає a constexpr? Якщо так, я можу побачити використання.
ereOn

20
Я вважаю, що в питанні повинно бути зазначено "навіщо вводити нове ключове слово (!), Якщо компілятор може сам зробити висновок, чи можна функцію оцінювати за час компіляції чи ні". Маючи це "гарантоване ключовим словом", це звучить добре, але я думаю, що я вважаю за краще, щоб це було гарантовано, коли це можливо, без необхідності використання ключового слова.
Кос

6
@Kos: Хтось, хто БІЛЬШЕ співрозмовник із внутрішніми мовами C ++, ймовірно, віддасть перевагу вашому питанню, але моє запитання походить з точки зору людини, яка раніше писала код C, але зовсім не знайома з ключовими словами C ++ 2011, а також деталями реалізації компілятора C ++ . Можливість міркувати про оптимізацію компілятора та виведення константних виразів є предметом для більш досвідченого запитання користувача, ніж цей.
Warren P

8
@Kos Я думав так само, як і ви, і відповідь, яку я придумав, була, без constexpr, як би ви (легко) знали, що компілятор насправді компілював функцію для вас? Я припускаю, що ви можете перевірити висновок збірки, щоб побачити, що він зробив, але простіше просто сказати компілятору, що вам потрібна така оптимізація, і якщо з якихось причин він не може зробити це для вас, він дасть вам гарну компіляцію, помилка замість того, щоб мовчки не вдалося оптимізувати, де ви очікували її оптимізації.
Джеремі Фріснер

3
@Kos: Можна сказати те саме const. Справді, уповноважена намір це корисно ! Розміри масиву є канонічним прикладом.
Гонки легкості на орбіті

Відповіді:


303

Припустимо, це робить щось трохи складніше.

constexpr int MeaningOfLife ( int a, int b ) { return a * b; }

const int meaningOfLife = MeaningOfLife( 6, 7 );

Тепер у вас є щось, що можна оцінити до постійної, зберігаючи хорошу читабельність і дозволяючи трохи складніше обробити, ніж просто встановити константу на число.

Це в основному добре допомагає ремонту, оскільки стає більш очевидним, що ви робите. Візьмемо max( a, b )для прикладу:

template< typename Type > constexpr Type max( Type a, Type b ) { return a < b ? b : a; }

Тут є досить простий вибір, але це означає, що якщо ви зателефонуєте maxз постійними значеннями, це явно розраховується під час компіляції, а не під час виконання.

Ще одним хорошим прикладом може бути DegreesToRadiansфункція. Усі знаходять градуси легше читати, ніж радіани. Хоча ви можете знати, що в радіанах 180 градусів, це набагато чіткіше написано так:

const float oneeighty = DegreesToRadians( 180.0f );

Тут багато хорошої інформації:

http://en.cppreference.com/w/cpp/language/constexpr


18
Відмінний момент - це сказати компілятору спробувати обчислити значення під час компіляції. Мені цікаво, чому const не забезпечує цю функціональність, коли конкретизовані конкретні оптимізації? Або це?
TamusJRoyce

11
@Tamus: Часто це буде, але його не зобов’язують. constexpr зобов'язує компілятор і, якщо він не зможе, виплюне помилку.
Гоц

20
Я бачу це зараз. Гріх (0,5) - інший. Це акуратно замінює C макроси.
Warren P

10
Я бачу це як нове запитання про інтерв'ю: Поясніть відмінності між ключовим словом const та constexpr.
Warren P

2
Як спосіб документування цього моменту для себе я написав аналогічний код, як вище, і знову з функцією "const", а не "constexpr". Оскільки я використовую Clang3.3, -pedantic-помилки та -std = c ++ 11 я очікував, що останній не буде компілюватися. Він компілювався та виконувався як у випадку "constexpr". Ви гадаєте, що це розширення кланг або відбувся перелом на специфікацію C ++ 11 з моменту відповіді на цю посаду?
Арбалест

144

Вступ

constexprне було введено як спосіб сказати реалізації, що щось можна оцінити в контексті, який вимагає постійного вираження ; відповідні реалізації змогли довести це до C ++ 11.

Щось реалізація не може довести, це наміри певного коду:

  • Що розробник хоче висловити з цією суттю?
  • Чи слід ми сліпо дозволяти використовувати код у константному виразі , лише тому, що він працює?

Яким був би світ constexpr?

Скажімо, ви розробляєте бібліотеку і розумієте, що хочете мати можливість обчислити суму кожного цілого числа в проміжку (0,N].

int f (int n) {
  return n > 0 ? n + f (n-1) : n;
}

Відсутність наміру

Компілятор може легко довести, що вищенаведена функція може бути викликана в постійному виразі якщо переданий аргумент відомий під час перекладу; але ви не заявили про це як про наміри - це просто так сталося.

Тепер хтось інший приходить разом, читає вашу функцію, робить той же аналіз, що і компілятор; " О, ця функція може бути використана в постійному виразі!" , і пише наступний фрагмент коду.

T arr[f(10)]; // freakin' magic

Оптимізація

Ви, як "дивовижний" розробник бібліотеки, вирішите, що fслід кешувати результат при виклику; хто хотів би обчислювати один і той же набір значень знову і знову?

int func (int n) { 
  static std::map<int, int> _cached;

  if (_cached.find (n) == _cached.end ()) 
    _cached[n] = n > 0 ? n + func (n-1) : n;

  return _cached[n];
}

Результат

Ввівши свою нерозумну оптимізацію, ви просто порушили кожне використання своєї функції, що трапилося в контексті, коли константа-вираз .

Ви ніколи не обіцяли, що функція може бути використана в постійному вираженні , і без неї constexprне було б можливості надати таку обіцянку.


Отже, навіщо нам це потрібно constexpr?

Основне використання constexpr - це оголошення намірів .

Якщо сутність не позначена як constexpr- вона ніколи не передбачалася використовувати в постійному виразі ; і навіть якщо це так, ми покладаємось на компілятора, щоб діагностувати такий контекст (тому що він нехтує нашими намірами).


25
Це, мабуть, правильна відповідь, оскільки останні зміни в C ++ 14 та C ++ 17 дозволяють використовувати набагато ширший діапазон мови у constexprвиразах. Іншими словами, майже все можна зауважити constexpr(можливо, одного дня це просто відійде через це?), І якщо у вас немає критерію, коли використовувати constexprчи ні, майже весь код буде записаний як такий .
alecov

4
@alecov Безумовно не все ... I/O, syscallі dynamic memory allocationбезумовно НЕ можите бути позначено як constexprКрім того, не всі повинно бути constexpr.
Цзяхао Сю

1
@alecov Деякі функції призначені для виконання під час виконання, і безглуздо це робити під час компіляції.
Цзяхао Сю

1
Мені також найкраще подобається ця відповідь. Оцінка часу компіляції - це акуратна оптимізація, але те, що ви справді отримуєте, constexpr- це запорука певної поведінки. Так само, як constі у нас.
Томаш Зато -

Який компілятор дозволяє цю версію, що не містить constexpr, int f (int n) { return n > 0 ? n + f (n-1) : n;} T arr[f(10)]; я не можу змусити цю компілювати ніде?
Джер

91

Візьміть std::numeric_limits<T>::max(): з будь-якої причини це метод. constexprбуло б вигідно тут.

Інший приклад: ви хочете оголосити C-масив (або a std::array), такий же великий, як інший масив. Наразі це зробити так:

int x[10];
int y[sizeof x / sizeof x[0]];

Але чи не було б краще вміти писати:

int y[size_of(x)];

Завдяки constexpr: Ви можете:

template <typename T, size_t N>
constexpr size_t size_of(T (&)[N]) {
    return N;
}

1
+1 для приємного використання шаблону, але він працює точно так само без constexpr, чи не так?
Кос

21
@Kos: Ні. Це поверне значення виконання. constexprзмушує компілятора змусити функцію повернути значення часу компіляції (якщо це можливо).
deft_code

14
@Kos: без цього constexprвін не може використовуватися в оголошенні розміру масиву, ні як аргумент шаблону, незалежно від того, результат виклику функції є константа часу компіляції чи ні. Ці два фактично є єдиними випадками використання для, constexprале принаймні важливих випадків аргументу шаблону є важливими.
Конрад Рудольф

2
"з будь-якої причини, це метод". Причина полягає в тому, що в C ++ 03 є лише цілі цілі часу компіляції, але немає інших типів часу компіляції, тому лише метод може працювати для довільних типів до C ++ 11.
Себастьян Мах

5
@LwCui Ні, це не "нормально": GCC за замовчуванням просто певний з певних речей. Використовуйте -pedanticопцію, і вона буде позначена як помилка.
Конрад Рудольф

19

constexprфункції дійсно приємні і чудове доповнення до c ++. Однак ви маєте рацію в тому, що більшість проблем, які вони вирішують, можна неефективно обробити макросами.

Однак одне із застосувань constexprне має C ++ 03 еквівалентних набраних констант.

// This is bad for obvious reasons.
#define ONE 1;

// This works most of the time but isn't fully typed.
enum { TWO = 2 };

// This doesn't compile
enum { pi = 3.1415f };

// This is a file local lvalue masquerading as a global
// rvalue.  It works most of the time.  But May subtly break
// with static initialization order issues, eg pi = 0 for some files.
static const float pi = 3.1415f;

// This is a true constant rvalue
constexpr float pi = 3.1415f;

// Haven't you always wanted to do this?
// constexpr std::string awesome = "oh yeah!!!";
// UPDATE: sadly std::string lacks a constexpr ctor

struct A
{
   static const int four = 4;
   static const int five = 5;
   constexpr int six = 6;
};

int main()
{
   &A::four; // linker error
   &A::six; // compiler error

   // EXTREMELY subtle linker error
   int i = rand()? A::four: A::five;
   // It not safe use static const class variables with the ternary operator!
}

//Adding this to any cpp file would fix the linker error.
//int A::four;
//int A::six;

12
Не могли б ви пояснити, що "НАЙЧАСНА тонка помилка лінкера"? Або хоча б надати покажчик на уточнення?
енобайрам

4
@enobayram, потрійний оператор приймає адресу операндів. Це не очевидно з коду. Все добре поєднується, але посилання виходить з ладу, оскільки адресу fourне вирішено. Мені довелося справді копатися, щоб зрозуміти, хто приймає адресу моєї static constзмінної.
deft_code

23
"Це погано з очевидних причин": найочевидніша причина - крапка з комою, правда?
TonyK

4
"ВИКЛЮЧНА тонка помилка лінкера" мене повністю здивувала. Ні, fourні fiveвони не мають сфери застосування.
Стівен Лу

3
див. також новий enum classтип, він виправляє деякі проблеми перерахування.
ninMonkey

14

З того, що я читав, потреба constexpr випливає з проблеми метапрограмування. Класи ознак можуть мати константи, представлені як функції, подумайте: numeric_limits :: max (). З Constexpr ці типи функцій можуть використовуватися в метапрограмуванні, або як межі масивів, тощо.

Інший приклад з верхньої частини моєї голови - це те, що для інтерфейсів класів вам може знадобитися, щоб похідні типи визначали власні константи для певної операції.

Редагувати:

Після того, як тикаєш на SO, схоже, що інші придумали кілька прикладів того, що може бути можливим для contexprs.


"Щоб бути частиною інтерфейсу, який ви повинні бути функцією"?
Даніель Ервікер

Тепер, коли я бачу корисність цього, я трохи більше схвильований щодо C ++ 0x. Здається, добре продумана річ. Я знав, що вони повинні бути. Ці мовні стандарти uber-geeks рідко роблять випадкові речі.
Warren P

Я набагато більше схвильований щодо лямбдів, моделі потоків, ініціалізатора_спису, посилань на переоцінку, варіативних шаблонів, нових перевантажень прив'язки ... На що чекати можна ще небагато.
luke

1
О так, але я вже розумію лямбда / закриття в декількох інших мовах. constexprбільш конкретно корисний у компіляторі з потужною системою оцінки вираження часу компіляції. C ++ дійсно не має однолітків у цьому домені. (це сильна похвала для C ++ 11, IMHO)
Warren P

11

З виступу Stroustrup на "Going Native 2012":

template<int M, int K, int S> struct Unit { // a unit in the MKS system
       enum { m=M, kg=K, s=S };
};

template<typename Unit> // a magnitude with a unit 
struct Value {
       double val;   // the magnitude 
       explicit Value(double d) : val(d) {} // construct a Value from a double 
};

using Speed = Value<Unit<1,0,-1>>;  // meters/second type
using Acceleration = Value<Unit<1,0,-2>>;  // meters/second/second type
using Second = Unit<0,0,1>;  // unit: sec
using Second2 = Unit<0,0,2>; // unit: second*second 

constexpr Value<Second> operator"" s(long double d)
   // a f-p literal suffixed by ‘s’
{
  return Value<Second> (d);  
}   

constexpr Value<Second2> operator"" s2(long double d)
  // a f-p literal  suffixed by ‘s2’ 
{
  return Value<Second2> (d); 
}

Speed sp1 = 100m/9.8s; // very fast for a human 
Speed sp2 = 100m/9.8s2; // error (m/s2 is acceleration)  
Speed sp3 = 100/9.8s; // error (speed is m/s and 100 has no unit) 
Acceleration acc = sp1/0.5s; // too fast for a human

2
Цей приклад можна знайти також у статті Stroustrup " Розробка програмного забезпечення для інфраструктури" .
Матьє Поулет

clang-3.3: помилка: тип повернення функції constexpr "Значення <Секунда>" не є буквальним типом
Mitja

Це добре, але хто ставить літерали в такий код. Якщо ваш компілятор "перевірить свої одиниці", ви мали би сенс, якби ви писали інтерактивний калькулятор.
bobobobo

5
@bobobobo або якщо ви писали навігаційне програмне забезпечення для Mars Climate Orbiter, можливо :)
Джеремі Фріснер

1
Щоб його скласти - 1. Використовуйте підкреслення в буквальних суфіксах. 2. додати оператора "" _m на 100_м. 3. використовуйте 100,0_m або додайте перевантаження, яка приймає неподписані довго. 4. Оголосіть конструктор Значення constexpr. 5. Додайте відповідного оператора / до класу Value на зразок цього: автоматичний оператор constexpr / (const Value <Y> та інше) const {return Value <Unit <TheUnit :: m - Value <Y> :: TheUnit :: m, TheUnit :: кг - Значення <Y> :: TheUnit :: кг, TheUnit :: s - Значення <Y> :: TheUnit :: s >> (val / other.val); }. Де TheUnit є typedef для Unit, доданого всередині класу Value.
0kcats

8

Ще одне використання (поки не згадується) - constexprконструктори. Це дозволяє створювати константи часу компіляції, які не потрібно ініціалізувати під час виконання.

const std::complex<double> meaning_of_imagination(0, 42); 

З'єднайте це з літералами, визначеними користувачем, і ви маєте повну підтримку літер, визначених користувачем.

3.14D + 42_i;

6

Раніше був шаблон з метапрограмуванням:

template<unsigned T>
struct Fact {
    enum Enum {
        VALUE = Fact<T-1>*T;
    };
};

template<>
struct Fact<1u> {
    enum Enum {
        VALUE = 1;
    };
};

// Fact<10>::VALUE is known be a compile-time constant

Я вважаю, що вам constexprбуло запропоновано писати такі конструкції без необхідності шаблонів і дивних конструкцій зі спеціалізацією, SFINAE та ін., Але точно так, як ви б написали функцію виконання, але з гарантією, що результат буде визначений у компіляції -час.

Однак зауважте, що:

int fact(unsigned n) {
    if (n==1) return 1;
    return fact(n-1)*n;
}

int main() {
    return fact(10);
}

Скомпілюйте це, g++ -O3і ви побачите, що fact(10)дійсно евалюється під час компіляції!

Компілятор, відомий VLA (таким чином, компілятор C в режимі C99 або компілятор C ++ з розширеннями C99) може навіть дозволити вам робити:

int main() {
    int tab[fact(10)];
    int tab2[std::max(20,30)];
}

Але те, що на даний момент це нестандартний C ++ - constexprвиглядає як спосіб боротьби з цим (навіть без VLA, у наведеному вище випадку). І досі існує проблема необхідності мати "формальні" постійні вирази як аргументи шаблону.


Функція факту не оцінюється під час компіляції. Він повинен бути constexpr і повинен мати лише одне повернення.
Суманний

1
@Sumant: Ви маєте рацію, що це не потрібно оцінювати під час компіляції, але це так! Я мав на увазі те, що насправді відбувається у компіляторах. Скомпілюйте його на останній GCC, перегляньте отриманий асм і перевірте, чи не вірите ви мені!
Кос

Спробуйте додати, std::array<int, fact(2)>і ви побачите, що факт () не оцінюється під час компіляції. Просто оптимізатор GCC робить гарну роботу.

1
Ось що я сказав ... чи я справді такий незрозумілий? Дивіться останній абзац
Кос

5

Щойно почали перемикати проект на c ++ 11 і натрапив на ідеально гарну ситуацію для constexpr, яка очищає альтернативні методи виконання тієї ж операції. Ключовим моментом тут є те, що ви можете розмістити функцію в декларації розміру масиву лише тоді, коли вона оголошена constexpr. Існує ряд ситуацій, коли я бачу, що це дуже корисно, рухаючись вперед із областю коду, в якій я задіяний.

constexpr size_t GetMaxIPV4StringLength()
{
    return ( sizeof( "255.255.255.255" ) );
}

void SomeIPFunction()
{
    char szIPAddress[ GetMaxIPV4StringLength() ];
    SomeIPGetFunction( szIPAddress );
}

4
Це однаково можна записати: const size_t MaxIPV4StringLength = sizeof ("255.255.255.255");
Superfly Jon

static inline constexpr const autoпевно, краще.
Цзяхао Сю

3

Всі інші відповіді чудові, я просто хочу навести класний приклад однієї речі, яку ви можете зробити з constexpr, що дивовижно. See-Phit ( https://github.com/rep-movsd/see-phit/blob/master/seephit.h ) - це HTML-аналізатор часу та механізм створення шаблонів. Це означає, що ви можете помістити HTML-код і отримати дерево, яким можна керувати. Проведення розбору, виконаного під час компіляції, може дати вам додаткову продуктивність.

На прикладі сторінки github:

#include <iostream>
#include "seephit.h"
using namespace std;



int main()
{
  constexpr auto parser =
    R"*(
    <span >
    <p  color="red" height='10' >{{name}} is a {{profession}} in {{city}}</p  >
    </span>
    )*"_html;

  spt::tree spt_tree(parser);

  spt::template_dict dct;
  dct["name"] = "Mary";
  dct["profession"] = "doctor";
  dct["city"] = "London";

  spt_tree.root.render(cerr, dct);
  cerr << endl;

  dct["city"] = "New York";
  dct["name"] = "John";
  dct["profession"] = "janitor";

  spt_tree.root.render(cerr, dct);
  cerr << endl;
}

1

Ваш основний приклад - це той самий аргумент, що і аргументи самих констант. Навіщо використовувати

static const int x = 5;
int arr[x];

над

int arr[5];

Тому що це спосіб більш ретельний. Використання constexpr набагато, набагато швидше для запису та читання, ніж існуючі методи метапрограмування.


0

Це може ввімкнути нові оптимізації. constтрадиційно є підказкою для системи типів і не може використовуватися для оптимізації (наприклад, constфункція-член може будь const_cast-коли змінювати об'єкт, юридично, тому constне можна довіряти для оптимізації).

constexprозначає, що вираз справді є постійним, за умови, що входи до функції const. Поміркуйте:

class MyInterface {
public:
    int GetNumber() const = 0;
};

Якщо це виявлено в якомусь іншому модулі, компілятор не може довіряти, що GetNumber()не повертатиме різні значення кожного разу, коли він буде викликаний - навіть послідовно, без викликів non-const між ними - тому що він constміг бути відкинутий під час реалізації. (Очевидно, будь-який програміст, який зробив це, повинен бути знятий, але мова дозволяє це, тому компілятор повинен дотримуватися правил.)

Додавання constexpr:

class MyInterface {
public:
    constexpr int GetNumber() const = 0;
};

Тепер компілятор може застосувати оптимізацію, коли повернене значення GetNumber()кешується та усунути додаткові виклики GetNumber(), оскільки constexprце більш сильна гарантія того, що значення повернення не зміниться.


Насправді const може бути використаний для оптимізації ... Це невизначена поведінка для зміни значення, визначеного const, навіть після const_castIIRC. Я б очікував, що він буде відповідати constфункціям членів, але мені потрібно перевірити це зі стандартом. Це означатиме, що компілятор може сміливо робити там оптимізації.
Кос

1
@Warren: Не має значення, чи оптимізація реально зроблена, вона просто дозволена. @Kos: це маловідома тонкощі, що якщо початковий об'єкт не був оголошений const ( int xvs. const int x), то його можна легко змінити, const_castвідмінивши const на вказівник / посилання на нього. В іншому випадку const_castзавжди буде викликатись невизначеною поведінкою і бути марним :) У цьому випадку компілятор не має інформації про постійність початкового об'єкта, тому він не може розповісти.
AshleysBrain

@Kos Я не думаю, що const_cast є єдиною проблемою тут. Методом const дозволено читати та навіть змінювати глобальну змінну. І навпаки, хтось із іншої нитки також може змінювати об'єкт const між викликами.
енобайрам

1
Тут "= 0" недійсний і його слід видалити. Я б це зробив сам, але не впевнений, що це відповідає протоколу SO.
KnowItAllWannabe

Обидва приклади недійсні: перший ( int GetNumber() const = 0;) повинен оголосити GetNumber()метод віртуальним. Другий ( constexpr int GetNumber() const = 0;) недійсний, оскільки чистий специфікатор ( = 0) передбачає, що метод є віртуальним, але constexpr не повинен бути віртуальним (ref: en.cppreference.com/w/cpp/language/constexpr )
stj

-1

Коли користуватися constexpr:

  1. всякий раз, коли є константа часу компіляції.

Хоча я згоден з вами, ця відповідь не пояснює, чому constexpr слід віддавати перевагу макросам препроцесора або const.
Sneftel

-3

Це корисно для чогось подібного

// constants:
const int MeaningOfLife = 42;

// constexpr-function:
constexpr int MeaningOfLife () { return 42; }

int some_arr[MeaningOfLife()];

Зв’яжіть це з класом ознак чи подібним, і це стане досить корисним.


4
У вашому прикладі він пропонує нульову перевагу перед звичайною константою, тому він насправді не відповідає на питання.
jalf

Це надуманий приклад, уявіть, якщо MeaningOfLife () отримує своє значення десь з іншого місця, скажімо іншу функцію або #define або серію therof. Ви можете не знати, що він повертає, це може бути код бібліотеки. Інші приклади, уявіть незмінний контейнер, який має метод розміру contexpr (). Тепер ви можете зробити int arr [container.size ()];
plivesey

2
@plivesey, чи можете ви відредагуйте свою відповідь тоді кращим прикладом.
Мукеш
Використовуючи наш веб-сайт, ви визнаєте, що прочитали та зрозуміли наші Політику щодо файлів cookie та Політику конфіденційності.
Licensed under cc by-sa 3.0 with attribution required.