На мое удивление, в большинстве мест, где я читал о constexpr, упоминаются самые очевидные и не интересные вещи. Например о том, какие ограничения существуют для constexpr функций, хотя по факту достаточно лишь сказать, что constexpr функция должна состоять из одного return стейтмента. А вот что на мой взгляд действительно важно знать.
DISCLARIMER: В посте вы часто встретите употребление английских слов и определений вместо русских, на мой взгляд так значительно удобнее для любого программиста, нежели маяться в попытках понять, что я имел ввиду под каким-нибудь дурацким переводом.
Место в грамматике
Интуитивно кажется, что constexpr это нечто похожее на const, а поэтому является частью типа в грамматике С++11, но это не так. На самом деле constexpr это совершенно отдельный declaration specifier. Вполне себе такой же как и typedef или friend, а поэтому может появляться именно там, где могут появляться эти два. Почему это важно? А потому что при работе с неимоверно сложным синтаксисом declarations в С++, стоит понимать, что правила которые действуют для const, не действуют для constexpr. Поясню на примере с указателями:
Любезный clang ясно дает понять, тип указателя p - int *const, вместо ожидаемого const int*. Так вот, собственно первый вывод: constexpr применяется к declaration в целом и не является частью типа. Если constexpr с указателями в общем-то редко используется, знать этот факт вдвойне важно при определении функций. Дело в том, что constexpr не является частью типа возвращаемого значения функции как это было бы в случае с const. Приведу опять пример с указателем и функциями, где этот факт четко иллюстрирован:
int number = 10;
constexpr int *number_addr() {
return &number;
}
int main(int argc, char **argv) {
int *p = number_addr();
return 0;
}
Вполне компилируется и работает. Согласен, что пример не особо полезен на практике, но позже я покажу почему важно это все знать. Бывают реально случаи, когда указатели используются в constexpr функциях. А пока давайте суммируем наконец то, как constexpr связан с const:
- При объявлении переменных, constexpr применяет const к самой переменной (а не к типу на который она указывает, если это указатель).
- При объявлении функций, constexpr не влияет на тип функции. В ход, однако, вступают ограничения, о которых все так любят писать.
- При объявлении функций-членов (они же методы), constexpr объявляет данную функцию-член константной. Не влияет на тип возвращаемого значения.
- При объявлении конструктора, constexpr как в случае с обычной функцией ничего не меняет, только включает ограничения.
Надеюсь, все выше сказанное помогло в какой-то мере понять каким образом constexpr применяется к сущностям языка и устронило недопомнимание на тему const vs constexpr.
Когда применять constexpr
На мой взгляд это второй по важности вопрос, который следует знать на зубок. Стоит правда отметить, что речь не пойдет о том, применять или нет, это вы решайте сами. Как правило применяется constexpr в тех случаях, когда нужно иметь возможность производить некие вычисления во время компиляции. В данной же секции, я объясняю механику применения, т.е. если вы решили применять constexpr, то как именно это следует делать.
Если коротко, то constexpr следует применять везде, где можно. И если достаточно понятно, когда применять constexpr к переменным и обычным функциям, то применение в классах требует небольшого пояснения. Рассмотрим пример:
class vec3 {
union {
struct {
float _x, _y, _z;
};
float _v[3];
};
public:
constexpr vec3(): _x(0), _y(0), _z(0) {}
constexpr vec3(float x, float y, float z): _x(x), _y(y), _z(z) {}
constexpr vec3(const vec3&) = default;
vec3 &operator=(const vec3&) = default;
constexpr float x() { return _x; }
constexpr float y() { return _y; }
constexpr float z() { return _z; }
constexpr const float *v() { return _v; }
float *v() { return _v; }
};
1. С конструкторами я думаю понятно. Если у конструктора пустое тело и есть возможность сделать его constexpr - делайте.
2. Default и delete члены тоже можно объявлять как constexpr. Вполне возможно, что они становятся constexpr при возможности по умолчанию, но для целей самодокументирования никогда не повредит явно указать это.
3. Оператор присвоения обычно не определяется как constexpr, потому что традицонно он возвращает не константную ссылку на *this. А как мы знаем из первой секции этого поста, constexpr делает функцию-член константной (this становится const). Просто не уместно, да и область применения сомнительна, хотя вполне возможно и этот оператор сделать constexpr.
4. Если кто-то еще не понял, объявление constexpr float x() { return _x; } внутри класса эквивалентно float x() const { return _x; }, а поэтому везде где нам хотелось бы определить константную функцию-член и где есть возможность сделать ее constexpr функцией, надо делать.
5. Не забываем важную информацию из первой секции поста - constexpr не влияет на тип возвращаемого значения функции, а поэтому constexpr const сочетание очень даже имеет смысл. Кроме того, т.к. constexpr неявно делает функцию-член константной, мы не можем вернуть float* используя const float _v[3].
6. Еще раз, учитывая, что (5) является константной функией-членом, можно сделать перегрузку для не константного случая, как это традиционно делается в С++ для акцессоров возвращающих указатель или ссылку. Так что все тут вполне корректно.
Заключение
Надеюсь данный пост разъясняет все основные, действительно важные моменты применения constexpr. По крайней мере у меня сложилось впечатление, что темных углов больше не осталось. Удачного применения,
да прибудет с вами сила.
Ссылка | Комментарии [2]