Константные выражения

 

Хотя C++ по праву считается одним из наиболее близких к железу, а значит и наиболее производительных языков программирование, сообщество не оставляет попыток выжать дополнительные крупицы производительности. Так, когда пришло осознание того, на что способны шаблоны очень много вычислений было перенесено со стадии выполнения в стадию компиляции. С тех пор прошло довольно много времени и сейчас в C++ появилось еще одно новшество, призванное перенести еще больше работы на этап компиляции. Речь, конечно же, идёт о constexpr, или константных выражениях, которые были представлены в C++11. О них и пойдёт речь в настоящей статье

Еще более константный

В C++ есть одна интересная деталь, которая удивляет очень многих - почему так писать можно:

struct A
{
    static const int integral = 50;
};

а так нельзя:

struct A
{
    static const float real = 50.0f;
};

Действительно, ограничение выглядит несколько несуразно. Внутри определения класса мы можем инициализировать статические константные члены только интегрального типа. Из-за этого ограничения мы имели все “плюсы” динамической инициализации наших констант не интегрального типа. Данную проблему призвано решить новое ключевое слово constexpr:

struct A
{
    static constexpr const float real = 50.0f;
};

Теперь мы имеем константу типа float на этапе компиляции. Более того мы можем создать константу этапа компиляции для любого(!) типа, который обладает определенными свойствами:

struct B
{
//Содержание класса B мы рассмотрим позже
...
};

struct A
{
    static constexpr const float real = 50.0f;
    static constexpr const B object{50};
};

Для объектов, ключевое слово constexpr полностью покрывает то, что придает им ключевое слово const, а следовательно в вышеприведённом коде const избыточен и может быть опущен без каких либо последствий.

Здесь и далее под объектом понимается как интегральная переменная, так и объект класса.

Может показаться, что const и constexpr близнецы-братья, но это не так: const говорит о том, что объект является неизменяемым, в то время как constexpr говорит, что объект является константой времени компиляции. Т.е. тогда как const говорит о логической константности объекта, constexpr говорит о его физической константности.

Конечно же нет в стандарте такого понятия как “физическая константность”, стандарт вообще не оперирует таким грубыми терминами и не определяет где и как система должна хранить ту или иную константу. Но, как показывает практика, константы времени компиляции хранятся в памяти не доступной для записи и, следовательно, являются физическими константами. Это мой, грубый термин, чтобы меньше писать.

Например:

#include <iostream>
using namespace std;

struct A
{
    static const int c_Answer; 
};

void modifier(const int& i, const int& j, const int& k, const int& m)
{
    int& ri = const_cast<int&>(i);
    int& rj = const_cast<int&>(j);
    int& rk = const_cast<int&>(k);
    int& rm = const_cast<int&>(m);
    ri = rj = rk = rm = 25;
}

int four()
{
    return 4;
}

int main()
{
    const int i = 1;
    constexpr int j = 2;
    int k = 3;
    const int m = four();
    modifier(i, j, k, m);
    std::cout << i << ", " << j << ", " << k << ", " << m;
    return 0;
}

Результат:

1, 2, 25, 25

Из вышеприведённого кода, вкупе с результатом, можно сделать вывод, что переменные i и j являются физическими константами, тогда как k и m оными не является. Хотя все они передаются в функции modifier() как константы, только i и j не могут быть изменены с помощью const_cast. При этом и i и m имеют одинаковые типы! Но есть между ними и разница, i инициализирована константным выражением, тогда как m – нет. Именно поэтому i имеет физическую константность(это гарантирует стандарт(3.6.2/2)) тогда как m может иметь оную, а может и не иметь(именно этот случай мы имеем тут). В случае с m всё зависит от компилятора и стандарт умывает руки. С constexpr, же, всё иначе: constexpr гарантировано имеет именно физическую константность. И чтобы это гарантировать constexpr может быть инициализировано только константным выражением, в то время как const может быть инициализировано чем угодно. Именно поэтому это не взаимозаменяемые понятия.

Вообще говоря использование const с теми же ограничениями, что и constexpr тоже даёт физическую константность объекту. В этом плане они различаются только тем, что constexpr заставляет соблюдать эти правила на уровне ошибок компилятора. Можно считать constexpr более проработанной версией const в части константности этапа компиляции(и, как следствие, физической константности).

Можно рассмотреть отличия constexpr от const по пунктам:

  1. constexpr позволяет инициализировать не-интегральные статические константы при определении класса.
  2. constexpr константа не может быть использована до того как определена.
  3. constexpr константа может быть инициализирована только константным выражением.
  4. constexpr гарантирует, что объект будет константой этапа компиляции, тогда как const не даёт таких гарантий.

Первое и четвёртое утверждения мы уже рассмотрели, теперь рассмотрим второе и третье:

struct A
{
    static const int c_Answer; 
};
...
const int c_HalfAnAnswer = A::c_Answer/2;//#1
const int A::c_Answer = 42;//#2

Здесь, выражение #1, может быть выполнено раньше выражения #2, что приведет к динамической инициализации c_HalfAnAnswer, вместо ожидаемой статической инициализации. Происходит это в силу того, что A::c_Answer/2 не является константным выражением.

Доказательство: Стандарт 5.19/2

an lvalue-to-rvalue conversion, unless it is applied to a non-volatile glvalue of integral or enumeration type that refers to a non-volatile const object with a preceding initialization, initialized with a constant expression

И, следовательно, #1 является динамически инициализируемой переменной если только компилятор не посчитает иначе. Но это его выбор, мы над ним не властны.

И это приводит нас сразу к третьему пункту: т.к. мы имеем не константное выражение, то это является препятствием для реализации пункта 3. Поэтому, чтобы выражение, при участие constexpr, было гарантированно константным - constexpr должна быть определена на момент использования:

struct A
{
    static constexpr int c_Answer = 42; 
};

const int c_HalfAnAnswer = A::c_Answer/2;

Константные функции

В отличии от константных объектов, для которых появление constexpr лишь формализовало то, что раньше существовало в более расплывчатых границах, constexpr для функций добавляет совершенно новый пласт, ранее аналогов не имевший. В C++ давно существует следующая проблема:

const int arraySize()
{
    return 5;
}

int main() 
{
    std::array<int, arraySize()> array;
    return 0;
}

Этот код, разумеется, не будет компилироваться. Но с другой стороны: функция arraySize() всегда возвращает константу, которая всегда известна на этапе компиляции. Мы это знаем, компилятор это знает, так в же тогда дело? Кто-то может сказать: “Это всё надуманно, можно ведь заменить функцию на объект!” Да, можно, но это выход только в данном, конкретном случае. А что скажите по поводу этого:

int main() 
{
    std::array<int, std::numeric_limits<char>::max()>;
    return 0;
}

Выглядит уже не очень надуманно, не правда ли? Подобная несуразность заставляла людей использовать CHAR_MAX и им подобные. Но если вы давно программируете на C++, то вы должны знать, что использование C-наследия не поощряется и использование этих макросов явно не добавляет элегантности вашему коду. А если перенести этот код под шаблоны, то макросы вообще никак помочь не смогут. Так вот, чтобы решить эту проблему в стандарт были добавлены константные функции. Поэтому с момента выходя C++11 оба вышеприведенный код(с numeric_limits) является вполне легальным. Первый, же, пример требует модификации:

constexpr int arraySize()
{
    return 5;
}

int main() 
{
    std::array<int, arraySize()> array;
    return 0;
}

И всё - добавили ключевое слово constexpr и функция arraySize() стала константной. Тоже самое произошло со всеми функциями numeric_limits, так что для нас это просто работает “из коробки”.

Хотя создание константных функций не сопряжено с какими либо трудностями, в плане синтаксиса, есть здесь и ложка дёгтя. Вполне логично предположить, что не каждая функция может быть сделана константной, т.к. многие функции зависят от внешних состояний или же меняют какие-то внешние состояния. Поэтому константной функцией может быть только такая функция, которая не имеет побочных эффектов(side effects). Формально, стандарт накладывает следующие ограничения на функции, которые претендуют на константность:

  • Функция должна возвращать значение являющееся литералом.
  • Аргументами функции должны быть только литералы.
  • Функция может содержать следующие конструкции: typedef, различные вариации using, static_assert, пустое выражение(;) и, собственно, return.
  • Функция может содержать не более одного return.

И дополнительно для constexpr функций-членов:

  • Метод не может быть виртуальным
  • Метод может быть объявлен с =default или c =delete
  • constexpr метод является const, по умолчанию

Литералом является результат любого константного выражения

Всего вышеперечисленного недостаточно, для того, чтобы гарантировать отсутствие побочных эффектов в функции и, следовательно, есть еще одно ограничение: выражение следующее за оператором return должно быть константным(а значит все подвыражения тоже должны быть константными). Но как определить, является ли выражение константным? Это четко определено в стандарте(5.19/2), который я не хочу здесь перепечатывать. Там всё предельно четко, ясно и лаконично изложено.

Рассмотрим несколько примеров:

int global;

constexpr void voidReturn()//#1
{
}

constexpr int increment(int x)//#2
{
    return x++;
}

constexpr int doubleGlobal()//#3
{
    return global + global;
}


constexpr float pi()//#4
{
    return 3.14f;
}

constexpr unsigned square(unsigned x)//#5
{
    return x*x;
}

constexpr int* addresOf(int variable)//#6
{
    return &variable;
}

int main()
{
    int* p = addresOf(global);//#7
    constexpr int* constP = addresOf(global);//#8
    return 0;
}

Здесь, примеры #1-3 и #8 не скомпилируются, тогда как остальные пройдут компиляцию б��з проблем. Сосредоточимся на некорректных определениях и вызове:

#1 – тут, я думаю, всё ясно это противоречит правилу определения constexpr функции приведенной выше.

#2 – здесь применяется инкремент, который не является константным выражением, в связи с чем функция не может быть константной.

#3 – здесь используется глобальный не константный объект, что сразу же делает выражение не константным.

#8 – тоже что в случае с #3

Интересным является случай #7, мы используем не константный глобальный объект совместно с constexpr функцией и… всё работает! А происходит это в силу того, что в данном случае функция не участвует в формировании константного выражения и, соответственно, может быть использована с не константными аргументами. Это “отбирает” константность у функции, но позволяет использовать функции как в константных так и в не константных выражения. Что может быть весьма удобно в использовании, например, вышеприведенной функции square():

constexpr unsigned tenSquare = square(10);

int main()
{
    unsigned fifty = 50;
    unsigned fiftySquare = square(fifty);
    return 0;
}

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

Где логика?

До сих пор мы рассматривали только простейшие constexpr функции, которые, по сути, не содержат никакой логики. Хотя ограничение наложенные на константные функции весьма существенны, всё же они позволяют добавить некоторую логику в функции. Достигнуть этого нам позволяют: тернарный оператор(?:), логическое И(&&) и логическое ИЛИ(||) благодаря их особым свойствам. Свойства эти, как вы наверное знаете, заключаются в следующем:

  • Тернарный оператор: condition ? A : B. В зависимости от condition будет выполнено выражение A или B, и его результат будет являться результатом выражения. При этом вычисляется только одно выражение. Второе выражение не учитывается. 
  • Логическое И: A && B. Если A ложь, то B не вычисляется.
  • Логическое ИЛИ: A || B. Если A истина, то B не вычисляется.

Эти правила, вкупе с правилом constexpr рассмотренном в случае #7, даёт нам интересный функционал. В качествен примера, рассмотрим функцию суммы последовательности натуральных чисел от 1 до N. При этом, помимо N, функция будет принимать степень члена ряда. На данный момент реализована сумма только для первой степени(но аргумент есть, как задел на будущее), а, следовательно другие степени должны быть отсечены еще на этапе компиляции :

#include <limits>
#include <iostream>

constexpr int sequenceSum(int n, int power)
{
    static_assert(power == 1,
        "Unsupported sequence power!");
    return n*(n + 1)/2;
}

int main() 
{
    constexpr int plainSum = sequenceSum(55, 1);//Нормально
    constexpr int squaresSum = sequenceSum(55, 2);//Ошибка
    return 0;
}

Вышеприведенная реализация вполне ясно выражает наше намерение и крайне лаконична. У неё есть только один недостаток – она не является корректным C++11 кодом. А всё из-за того, что sequenceSum() принимает аргумент, константность которого не гарантируется. Можно было бы вынести аргумент функции в аргумент шаблона, но это лишило бы нас возможности использовать sequenceSum() с не-константным аргументом. Чтобы решить эту задачу мы применим тернарный оператор:

#include <limits>
#include <iostream>

constexpr int sequenceSum(int n, int power)
{
    return power == 1 ? n*(n + 1)/2 : throw std::invalid_argument("");
}

int main() 
{
    constexpr int plainSum = sequenceSum(55, 1);//Нормально
    constexpr int squaresSum = sequenceSum(55, 2);//Ошибка
    return 0;
}

Теперь мы получили то, что хотели: при некорректном втором аргументе мы имеем ошибку времени компиляции для константного выражения и исключение для не константного. Как это работает? Очень просто – тут используется свойство тернарного оператора, который вычисляет выражение только если оно будет выбрано. Таким образом, если power == 1 является ложным, то тернарный оператор выполняет выражение throw std::invalid_argument(""), которое не является константным о чем компилятор нам с готовностью и сообщает. Конечно, мы могли бы использовать другие варианты превращения sequenceSum() в не-константное выражение, но они не были бы корректными вариантами для времени исполнения. Исключения здесь подходят как нельзя лучше. Вот, например, вариант с reinterpret_cast(который также запрещен в константных выражениях):

constexpr int sequenceSum(int n, int power)
{
    return power == 1 ? n*(n + 1)/2 : reinterpret_cast<int>(power);
}

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

Вышеприведенный пример является довольно простым, но программист не ограничен только одним тернарным оператором, их может быть великое множество, включая и другие, вышеупомянутые, логические операторы. В качестве примера более сложной конструкции добавим к вышеописанной функции еще 2 степени, так, что допустимыми степенями теперь будут является 1-3:

#include <limits>
#include <iostream>

constexpr int sequenceSumFirst(int n)
{
    return n*(n + 1)/2;
}

constexpr int sequenceSumSecond(int n)
{
    return n*(n + 1)*(2*n + 1)/6;
}

constexpr int sequenceSumThird(int n)
{
    return n*n*(n + 1)*(n + 1)/4;
}

constexpr int sequenceSum(int n, int power)
{
    return power > 0 && power < 4 ? power == 1 ?
    sequenceSumFirst(n) : power == 2 ? sequenceSumSecond(n) :
    sequenceSumThird(n)  : throw std::invalid_argument("");
}

int main() 
{
    constexpr int plainSum = sequenceSum(55, 1);//Нормально
    constexpr int squaresSum = sequenceSum(55, 2);//Нормально
    constexpr int cubesSum = sequenceSum(55, 3);//Нормально
    constexpr int unknownSum = sequenceSum(55, 4);//Ошибка
    return 0;
}

Выглядит уже не так просто, не правда ли? Добавим скобки для наглядности:

constexpr int sequenceSum(int n, int power)
{
    return power > 0 && power < 4 ? 
    (power == 1 ? sequenceSumFirst(n) : 
    (power == 2 ? sequenceSumSecond(n) : sequenceSumThird(n))) 
    : throw std::invalid_argument("");
}

Уже лучше, но код всё равно трудно читаем. Из этого можно сделать вывод: возможности, возможностями, но чрезвычайно умный код лучше приберечь для действительно важных мест. А ведь то, что я привёл не является действительно сложным примером. Я уверен вы можете придумать куда более сложные конструкции. Но не надо увлекаться.

Рекурсия

Еще одним важным свойством  constexpr функций является возможность само-вызова или, другими словами, – рекурсивность. При использовании подобных функций в динамическом контексте они ничем не отличаются от обычных функций. Тогда как при использовании в константных выражениях глубина рекурсии ограничена и определяется разработчиками компиляторов. Здесь все честно – динамическая рекурсия ограничена размером стека, “статическая” ограничена глубиной, которая зависит от компилятора. Приведём типичный пример для рекурсии – факториал:

#include <iostream>
using namespace std;

using ull = unsigned long long;

constexpr ull factorial(ull number)
{
    return number > 1 ? factorial(number - 1)*number : 1;
}

int main() 
{
    constexpr ull tenFactorial = factorial(10);
    std::cout << tenFactorial;
    return 0;
}

Как видно из кода выше – довольно легко получить рекурсивный подсчёт выполняющийся на этапе компиляции.

Опять ничего нового?!

Не то чтобы мы не имели возможности сделать всё то, что описано выше про constexpr функции, напротив, у нас были шаблоны! Все эти проблемы решались шаблонами, в том или ином виде. Но, согласитесь, использование constexpr функций облагораживает код, делая его яснее. Известно, что, зачастую, код с шаблонами приходится дешифровывать, в то время как constexpr функции довольно легки в понимании и не отвлекают программиста ненужными деталями. Т.е. шаблоны можно “всунуть” почти куда угодно, но стоит ли? Да, шаблоны оказались настолько мощной функциональностью, что метапрограммирование стало языком в языке. Функционально от этого язык, безусловно, выиграл, но теперь пришла пора добавить несколько более специализированных инструментов в язык, которые бы смотрелись на своём месте как в момент написания, так и в момент использования. constexpr функции одна из таких вещей. К тому же, если использовать подход с шаблонами, то каждая функция должна иметь 2 реализации: для этапа компиляции и для времени исполнения программы.

Исходя из вышенаписанного можно вывести еще один вариант использования constexpr функций – замена, набивших оскомину, идиоматических выражений типа std::is_base_of<T>::value на что-то более удобоваримое. Все подобные конструкции могут быть с лёгкостью обёрнуты в constexpr функции, что, во-первых, уменьшит количество писанины и, во-вторых, позволит убрать ненужные детали от пользователя. К примеру:

#include <type_traits>

template<class Base, class Derived>
constexpr bool isBaseOf()
{
    return std::is_base_of<Base, Derived>::value; 
}

class A {};
 
class B : A {};

int main() 
{
    static_assert(std::is_base_of<A, B>::value, "");
    static_assert(isBaseOf<A, B>(), "");
}

Кто-то может сказать, что невелик выигрыш, но, на мой взгляд, код с вызовом функции куда более нагляден, чем непонятное ::value. Помимо всего прочего, constexpr функции являются хорошим дополнением к шаблонам позволяя сделать код, использующий оные, чуть более опрятным и понятным.

Константные объекты

Еще одним способом применения constexpr является использование этого ключевого слова с конструктором. Хотя конструктор, по сути, является функцией всё же для него действуют особые правила и в случае с constexpr никаких исключений. Объявление константного конструктора выглядит предельно просто:

class SomeConstantClass
{
    constexpr SomeClass(){}
};

Для constexpr конструктора действуют все те же ограничения, что и для функции, но т.к. конструктор не возвращает значений, то и return запрещен внутри оного. Поэтому тело конструктора должно быть пустым(если не считать всевозможные static_assert, using и typedef. А какой в нём смысл, если он ничего не может? Не совсем так, кое-что он всё таки может – он может содержать список инициализации:

class Rectangle
{
public:
    constexpr Rectangle(int x, int y, uint width = 1, uint height = 1):
        m_X(x),
        m_Y(y),
        m_Width(width),
        m_Height(height)
    {
    }
private:
    int m_X;
    int m_Y;
    uint m_Width;
    uint m_Height;
};

Кроме того, мы можем использовать любые константные функции в списке инициализации, включая другой конструктор:

#include <stdexcept>

class Rectangle
{
public:
    constexpr Rectangle(int x, int y, uint width = 1, uint height = 1);
    constexpr Rectangle(uint width, uint height):
        Rectangle(0, 0, width, height)
    {
    }
    constexpr uint width()
    {
        return m_Width;
    }
    constexpr uint height()
    {
        return m_Height;
    }
private:
    constexpr uint _checkSize(uint size)
    {
        return size < 1000000 ? size : 
            throw std::out_of_range("Too big size");
    }
private:
    int m_X;
    int m_Y;
    uint m_Width;
    uint m_Height;
};

constexpr Rectangle::Rectangle(int x, int y, uint width, uint height):
    m_X(x),
    m_Y(y),
    m_Width(_checkSize(width)),
    m_Height(_checkSize(height))
{
}

В связи с классом можно рассмотреть интересный случай когда одно коварное правило не даёт нам написать код который, казалось бы, выглядит вполне приличным:

class UniverseGuru
{
public:
    static constexpr int getAnswer()
    {
        return 42;
    }
private:
    static constexpr int m_Answer = getAnswer();//Ошибка компиляции!
};

static constexpr int g_Answer = UniverseGuru::getAnswer();

Как видно из примера, мы не можем инициализировать статическую константную переменную constexpr методом класса. А всё “благодаря” правилу, что constexpr функция не может быть использована до того как определена. “Но постойте, она же определена” – может возопить кто-то. Стандарт по этому поводу не согласен – класс считается определенным по завершающей скобке(Стандарт 9.2/2) и, соответственно, метод getAnswer() не является определенным на этапе инициализации нашей статической переменной внутри класса, но является таковым при инициализации g_Answer. При этом класс, а соответственно и метод, считается определенным в следующих контекстах: в теле метода, при инициализации не константного члена класса и в аргументах по умолчанию. Часть из этого было продемонстрировано в примере с Rectangle, где мы использовали constexpr метод и делегирующий конструктор.

Как и в случае с функциями мы можем использовать constexpr конструктор как в константных выражениях, так и в “обычной жизни”. Например, мы можем создать 2 объекта Rectangle: один константный, а другой нет:

constexpr Rectangle rec{1, 2, 10, 15};
Rectangle square{50, 50};

Помимо ограничений непосредственно на конструктор, стандарт, также, воспрещает иметь виртуальных предков классу, который хочет иметь constexpr конструктор.

Если с предназначением констант и функций всё было , худо-бедно,понятно, то зачем нам нужен константный конструктор? Мне известны два применения данной функциональности:

  • Использование глобальных константных объектов с понятным временем инициализации.
  • Пользовательские литералы

Пользовательские литералы не будут рассмотрены в рамках данной статьи. Мы рассмотрим их отдельно в одной из будущих статей. Поэтому перейдём к использование глобальных констант и начнём с рассмотрения что-же не так сейчас.

Предположим, что у нас есть два файла(foo.cpp и main.cpp) со следующим содержанием:

foo.cpp:

std::string g_AppName{"WTF Resolver"};

main.cpp:

extern std::string g_AppName;
std::string g_AppString = g_AppName;
int main() 
{
    g_AppString += "v1.0.1";
    std::cout << g_AppString << std::endl;
    return 0;
};

Что будет выведено на экран? Правильно, – мы не знаем! Всё это “благодаря” отсутствию каких либо правил по порядку инициализации объектов между модулями. Поэтому мы можем получить как пустую строку g_AppString, до старта main(), так и содержащую g_AppName. Нет никаких гарантий(кто еще не знает, что глобальные переменные чрезвычайно опасны?). Так вот, с появлением constexpr конструкторов эта проблема частично решается. Теперь, если создается глобальный объект с помощью constexpr конструктора, и все аргументы конструктора тоже являются constexpr, тогда объект будет создан во время компиляции. Если заменить предыдущий пример на Rectangle(который имеет требуемый конструктор), то мы получим гарантированный вывод:

foo.cpp:

Rectangle g_RedSquare{330u, 70u};

main.cpp:

extern Rectangle g_RedSquare;
Rectangle g_NewSquare{g_RedSquare};

int main() 
{
    std::cout << "New square size will be: " << g_NewSquare.width() << "x" 
                   << g_NewSquare.height();
    return 0;
}

Вывод:

330x70

Заметьте, что g_RedSquare не constexpr и даже не обычная константа, этот объект можно модифицировать невозбранно и, тем не менее, он инициализирован на этапе компиляции! И подобное поведение гарантировано стандартом(3.6.2/2)

Что дальше?

Так как constexpr довольна молодая часть языка, то и её применение пока еще полностью не раскрыто. Но можно уже с уверенностью говорить, что свою нишу это часть занимаете довольно быстро – перенос вычислений времени компиляции с громоздких шаблонов в более гибкие constexpr функции. Во что это всё выльется покажет время, пока же, предлагаю рассмотреть, что предложено для включения в следующую версию стандарта, который обещает появится в 2014 году.

Больше свободы

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

  • Разрешить определение локальных переменных в constexpr функциях с последующим использованием оных. Исключением являются static и thread_local объекты инициализированные не константным выражением; они, всё так же, будут запрещены.
  • Разрешить использование switch и if/else/else if
  • Разрешить использование циклов, включая новый for(:).
  • Разрешить изменение локальных объектов. Это позволит, например, делать инкремент; без которого не мыслим ни один цикл.

Еще одним предложенным изменением, которое, без сомнения, будет принято является изменение поведение constexpr относительно функции-члена. Начиная с C++14 constexpr метод больше не будет являться const автоматически. Автору класса придется помечать функцию как const самостоятельно.

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

Что из этого перекочует в финальную версию стандарта  - покажет время. А раз речь зашла о C++14 не могу не сказать, что constexpr функции очень хорошо себя показали в связке с концептами, о которых мы поговорим как-нибудь в другой раз. Что еще раз доказывает их большой потенциал, а значит мы будем видеть всё больше кода с применением consexpr, что не может не радовать.