Спецификатор constexpr в C++11 и в C++14

  • Tutorial
Одна из новых возможностей C++11 — спецификатор constexpr. С помощью него можно создавать переменные, функции и даже объекты, которые будут рассчитаны на этапе компиляции. Это удобно, ведь раньше для таких целей приходилось использовать шаблоны. Но тут все не так просто. У тех, кто не так хорошо знаком с constexpr, может сложиться впечатление, что теперь не будет никаких проблем с расчетами на этапе компиляции. Но на constexpr-выражения наложены серьезные ограничения.

В первой части будет рассказано про constexpr, о том, какие будут изменения в стандарте C++14, а во второй части будет пример использования constexpr: библиотека, которая считает результат математического выражения в строке.
С помощью нее можно будет написать следующий код:
constexpr auto x = "(4^2-9)/8+2/3"_solve;
std::cout << "Answer is " << x;

И ответ в виде дроби будет получен на этапе компиляции:
Answer is 37/24
Сразу предупреждаю, код этой библиотеки сложно понять.
Кому эта тема интересна, добро пожаловать под кат!

Что такое constexpr?


Сначала пара слов о том, что вообще такое спецификатор constexpr. Как уже было сказано, с помощью него можно производить какие-то операции на этапе компиляции. Выглядит это так:
constexpr int sum (int a, int b)
{
	return a + b;
}

void func()
{
	constexpr int c = sum (5, 12); // значение переменной будет посчитано на этапе компиляции
}

constexpr-функция


constexpr возвращаемое_значение имя_функции (параметры)
Ключевое слово constexpr, добавленное в C++11, перед функцией означает, что если значения параметров возможно посчитать на этапе компиляции, то возвращаемое значение также должно посчитаться на этапе компиляции. Если значение хотя бы одного параметра будет неизвестно на этапе компиляции, то функция будет запущена в runtime (а не будет выведена ошибка компиляции).

constexpr-переменная


constexpr тип = expression;
Ключевое слово в данном случае означает создание константы. Причем expression должно быть известно на этапе компиляции.

Рассмотрим такой пример:
int sum (int a, int b)
{
	return a + b;
}

constexpr int new_sum (int a, int b)
{
	return a + b;
}

void func()
{
	constexpr int a1 = new_sum (5, 12); // ОК: constexpr-переменная
	constexpr int a2 = sum (5, 12); // ошибка: функция sum не является constexp-выражением
	int a3 = new_sum (5, 12); // ОК: функция будет вызвана на этапе компиляции
	int a4 = sum (5, 12); // ОК
}


constexpr-переменная является константой (const), но константа не является constexpr-переменной.

В случае «утери» constexpr-спецификатора переменной вернуть обратно его уже не получится, даже если значение может посчитаться на этапе компиляции. constexpr-спецификатор нельзя добавить с помощью const_cast, так как constexpr не является cv-спецификатором (это const и volatile). Такой код не заработает:
constexpr int inc (int a)
{
	return a + 1;
}

void func()
{
	int a = inc (3);
	constexpr int b = inc (a); // ошибка: a не является constexpr-выражением, из-за чего возвращаемое значение не имеет спецификатор constexpr
}


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

Также constexpr-функции могут работать с классами, это будет рассмотрено позже.

GCC, начиная с версии 4.4, поддерживает constexpr-функции, Clang также поддерживает с версии 2.9, а Visual Studio 2013 не поддерживает (но в Visual Studio «14» CTP наконец добавили поддержку).

Ограничения


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

Начнем с ограничений constexpr-переменных. Тип constexpr-переменной должен быть литеральным типом, то есть одним из следующих:
  • Скалярный тип
  • Указатель
  • Массив скалярных типов
  • Класс, который удовлетворяет следующим условиям:
    • Имеет деструктор по умолчанию
    • Все нестатические члены класса должны быть литеральными типами
    • Класс должен иметь хотя бы один constexpr-конструктор (но не конструктор копирования и перемещения) или не иметь конструкторов вовсе


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

Тут вроде ничего необычного. Основные ограничения наложены на constexpr-функции:
  • Она не может быть виртуальной (virtual)
  • Она должна возвращать литеральный тип (void вернуть нельзя*)
  • Все параметры должны иметь литеральный тип
  • Тело функции должно содержать только следующее:
    • static_assert
    • typedef или using, которые объявляют все типы, кроме классов и перечислений (enum)
    • using для указания видимости имен или пространств имен (namespace)
    • Ровно один return, который может содержать только литералы или constexpr-переменные и constexpr-функции


* С C++14 void также будет литеральным типом.

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

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

Конечно, все равно все эти возможности можно реализовать. Вместо циклов использовать рекурсию, вместо конструкции if-else — оператор «? :», а вместо создания переменных использовать значения функции.

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

Но тут у C++ большие проблемы с синтаксисом: анонимные функции нельзя использовать, все действия функции являются одним длинным выражением, а с добавлением оператора «? :» код вовсе становится нечитабельным. Также все это сопровождается непонятными сообщениями об ошибке, которые могут занимать сотни строк.

Но на этом проблемы не заканчиваются. Когда пишешь какую-то constexpr-функцию, которую потом будут часто использовать, хорошо бы возвращать читабельную ошибку. Тут можно ошибочно предположить, что static_assert как раз для этого подходит. Но static_assert использовать не получится, так как параметры функций не могут быть constexpr, из-за чего значения параметров не гарантированно будут известны на этапе компиляции.
Как же выводить ошибки? Единственный более-менее нормальный способ, который я нашел, заключается в выбрасывании исключения:
constexpr int div (int x, int y)
{
	return (y == 0) ? throw std::logic_error ("x can't be zero") : (y / x);
}

В случае вызова функции во время компиляции мы увидим ошибку, что конструкция throw не может находиться в constexpr-функции, а в runtime функция выбросит исключение.
Ошибку сложно будет найти, но хоть что-то.
Пример ошибки в gcc 4.8.2
Main.cpp:16:24: in constexpr expansion of ‘MathCpp::operator"" _solve(((const char*)"(67+987^(7-3*2))*(34-123)+17^2/0+(-1)"), 37ul)’
MathCpp.h:115:28: in constexpr expansion of ‘MathCpp::solve(str, ((size_t)size))’
MathCpp.h:120:103: in constexpr expansion of ‘MathCpp::get_addsub(MathCpp::SMathData(str, ((int)size), 0))’
MathCpp.h:209:89: in constexpr expansion of ‘MathCpp::_get_addsub(data.MathCpp::SMathData::create((((int)MathCpp::get_muldiv(data).MathCpp::SMathValue::end) + 1)), MathCpp::get_muldiv(data).MathCpp::SMathValue::value)’
MathCpp.h:217:50: in constexpr expansion of ‘MathCpp::get_muldiv(data.MathCpp::SMathData::create((((int)data.MathCpp::SMathData::start) + 1)))’
MathCpp.h:181:83: in constexpr expansion of ‘MathCpp::_get_muldiv(data.MathCpp::SMathData::create((((int)MathCpp::get_pow(data).MathCpp::SMathValue::end) + 1)), MathCpp::get_pow(data).MathCpp::SMathValue::value)’
MathCpp.h:38:111: error: expression ‘<throw-expression>’ is not a constant-expression
#define math_assert(condition,description) ((condition)? INVALID_VALUE: (throw std::logic_error (description), INVALID_VALUE))
^
MathCpp.h:195:15: note: in expansion of macro ‘math_assert’
? math_assert (false, «Division by zero»)

Такой способ вывода ошибки еще не соответствует стандарту языка, ничего не запрещает компилятору всегда выдавать ошибку о том, что нельзя использовать throw в constexpr-функции. В GCC 4.8.2 это работает, а в Visual Studio «14» CTP C++ compiler — уже нет.

В итоге сложно писать, сложно отлаживать, сложно понимать такие конструкции.
Но все не так плохо, в C++14 очень многие ограничения уберут.

Изменения в C++14


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

Второе незначительное изменение заключается в том, что теперь constexpr функции-члены класса не являются константными.
В C++11 следующие строчки были равносильными, а в С++14 это уже не так:
class car
{
	constexpr int foo (int a); // C++11: функция неявно получает спецификатор const, C++14 - не получает
	constexpr int foo (int a) const;
};

Объяснение этому можно найти тут.

И наконец, главное изменение разрешает использовать почти любые конструкции в constexpr функциях и конструкторах.
Теперь тело constexpr-функции может содержать любые конструкции, кроме:
  • Ассемблерных вставок
  • Ключевого слова goto
  • Определения переменных нелитерального типа или static и thread_safe-переменных. Все переменные должны инициализироваться при определении.


А тело constexpr-конструктора теперь должно удовлетворять более лояльным условиям:
  • Он должен соответствовать всем условиям constexpr-функции
  • Все его нестатические члены должны иметь литеральный тип
  • Аналогичное условие про то, что все нестатические члены класса должны каким-либо способом инициализироваться
  • Появилась возможность использовать union'ы, но с некоторыми ограничениями


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

Пример использования constexpr на C++11


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

Итак, мы хотим, чтобы можно было писать такой код:
constexpr auto n = "(67+987^(7-3*2))*(34-123)+17^2+(-1)"_solve;
std::cout << "Answer is " << n;

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

Объявляется пользовательский литерал таким образом:
constexpr int operator "" _solve (const char* str, const size_t size);
constexpr int solve (const char* str, const size_t size);

constexpr int operator "" _solve (const char* str, const size_t size)
{
    return solve (str, size);
}

В качестве ассерта будет использоваться следующий макрос:
#define math_assert(condition,description) ((condition) ? 0 : (throw std::logic_error (description), 0))

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

Приоритеты операторов будут такие (от более высоких к более низким):
  1. Сложение и вычитание
  2. Умножение и деление
  3. Возведение в целую степень

Функции считывания числа, степени, суммы и так далее будут принимать один параметр: структуру SMathData. В ней хранятся строка, ее размер и переменная start — откуда надо начинать читать:
struct SMathData
{
    constexpr SMathData (const char* _str, const int _size, const int _start) :
        str (_str), 
        size (_size), 
        start (_start)
    {}
    
    constexpr SMathData create (const int _start) const
    {
        return SMathData (str, size, _start);
    }
    
    constexpr char char_start() const
    {
        return char_at (start);
    }
    constexpr char char_at (const int pos) const
    {
        return (pos >= 0 && pos < size) ? str[pos] : 
            ((pos == size) ? 0 : 
                (math_assert (false, "Internal error: out of bounds"), 0));
    }
    
    const char* str;
    const int size;
    const int start;
};

А возвращать эти функции будут структуру SMathValue. В ней хранятся посчитанное значение и end — переменная, в которую записан конец числа, суммы, произведения или чего-то еще:
struct SMathValue
{
    constexpr SMathValue (const int _value, const int _end) :
        value (_value), 
        end (_end)
    {}
    
    constexpr SMathValue add_end (int dend) const
    {
        return SMathValue (value, end + dend);
    }
    
    const int value;
    const int end;
};


Для считывания числа будут 3 функции (одна основная и две вспомогательных):
// Считывает число (поддерживается унарный минус).
constexpr SMathValue get_number (const SMathData data);
// Рекурсивная функция считывания числа с его конца (без унарного минуса и проверок).
// Если positive == true, то функция вернет положительное число, а если false - то отрицательное. i - индекс цифры в строке.
constexpr SMathValue _get_number (const SMathData data, const int i, const bool positive);
// Возвращает индекс последней цифры числа в строке (start - начало числа).
constexpr int _get_number_end (const SMathData data);

constexpr SMathValue get_number (const SMathData data)
{
    return (data.char_start() == '-') ? 
        (math_assert (data.char_at (data.start + 1) >= '0' && data.char_at (data.start + 1) <= '9', "Not a digit"), 
            _get_number (data.create (data.start + 1), _get_number_end (data.create (data.start + 1)), false)) : 
        (math_assert (data.char_start() >= '0' && data.char_start() <= '9', "Digit required"), 
            _get_number (data, _get_number_end (data), true));
}

constexpr SMathValue _get_number (const SMathData data, const int i, const bool positive)
{
    return
        (i >= data.start) ? 
            SMathValue (_get_number (data, i - 1, positive).value * 10 + 
                (positive ? 1 : -1) * (data.char_at (i) - '0'), i)
        : SMathValue (0, data.start - 1);
}

constexpr int _get_number_end (const SMathData data)
{
    return (data.char_start() >= '0' && data.char_start() <= '9') ? 
            _get_number_end (data.create (data.start + 1)) : (data.start - 1);
}

Вот такая запутанная конструкция получается. get_number проверяет, что на текущем индексе действительно число и вызывает _get_number, передавая в качестве первой итерации конец числа (число читается справа налево).

Работа со скобками:
// get branum - сокращение от get bracket or number.
constexpr SMathValue get_branum (const SMathData data);

constexpr SMathValue get_branum (const SMathData data)
{
    return (data.char_start() == '(') ? 
        (math_assert (data.char_at (get_addsub (data.create (data.start + 1)).end + 1) == ')', "')' required"), 
         get_addsub (data.create (data.start + 1)).add_end (1))
        : get_number (data);
}

Если на текущем индексе число, то функция вызывает get_number, в противном случае функция считает выражение в скобках.

Дальше идет функция возведения в степень:
// Возвращает значение после возведения в степень.
constexpr SMathValue get_pow (const SMathData data);
// Вспомогательная функция. Тут предполагается, что start ссылается на следующий символ после конца первого числа (или выражения),
// то есть на символ '^', если он присутствует. value - значение первого числа (или выражения).
constexpr SMathValue _get_pow (const SMathData data, const int value);

constexpr SMathValue get_pow (const SMathData data)
{
    return _get_pow (data.create (get_branum (data).end + 1), get_branum (data).value);
}

constexpr SMathValue _get_pow (const SMathData data, const int value)
{
    return (data.char_start() == '^') ? 
        _get_pow (data.create 
        // start
        (get_branum (data.create (data.start + 1)).end + 1),     
        // value
        math_pow (value, get_branum (data.create (data.start + 1)).value))
        : SMathValue (value, data.start - 1);
}

В функции _get_pow проверяется, что текущий символ '^'. Если это так, то функция вызывает сама себя (точнее get_pow), передав туда новое значение, равное value в степени прочитанное_значение.

Получается, что строка "25" правильно обработается, если для нее вызвать get_pow. Так как в этом случае просто прочитается число, после чего оно вернется.
math_pow — простая constexpr-функция возведения в целую степень.
Реализация math_pow
constexpr int math_pow (const int x, const int y);
constexpr int _math_pow (const int x, const int y, const int value);

constexpr int math_pow (const int x, const int y)
{
    return math_assert (y >= 0, "Power can't be negative"), 
        _math_pow (x, y.to_int(), 1);
}

constexpr int _math_pow (const int x, const int y, const int value)
{
    return (y == 0) ? value : (x * _math_pow (x, y - 1, value));
}


Произведение и деление обрабатываются в одной функции:
// Возвращает результат после умножения и деления.
constexpr SMathValue get_muldiv (const SMathData data);
// Вспомогательная функция. Аналогична _get_pow.
constexpr SMathValue _get_muldiv (const SMathData data, const int value);

constexpr SMathValue get_muldiv (const SMathData data)
{
    return _get_muldiv (data.create (get_pow (data).end + 1), get_pow (data).value);
}

constexpr SMathValue _get_muldiv (const SMathData data, const int value)
{
    return (data.char_start() == '*') ? 
        _get_muldiv (data.create
        // start
        (get_pow (data.create (data.start + 1)).end + 1),     
        // value
        value * get_pow (data.create (data.start + 1)).value)
        
        : ((data.char_start() == '/') ? 
        (get_pow (data.create (data.start + 1)).value == 0)
            ? math_assert (false, "Division by zero")
        : _get_muldiv (data.create
        // start
        (get_pow (data.create (data.start + 1)).end + 1),     
        // value
        value / get_pow (data.create (data.start + 1)).value)       
        
        : SMathValue (value, data.start - 1));
}

Довольно сложно понять эту конструкцию, писать ее также затруднительно. Тут идет проверка, является ли текущий символ '*', если это так, то функция вызывает сама себя, перемножая value на прочитанное число (или выражение). В случае с '/' функция ведет себя аналогично, только перед этим идет проверка на то, что знаменатель не равен нулю. Если текущий символ не является '*' или '/', то просто возвращается значение.

Аналогично происходит с суммой и разностью:
Реализация get_addsub
constexpr SMathValue get_addsub (const SMathData data);
constexpr SMathValue _get_addsub (const SMathData data, const CMathVariable value);

constexpr SMathValue get_addsub (const SMathData data)
{
    return _get_addsub (data.create (get_muldiv (data).end + 1), get_muldiv (data).value);
}

constexpr SMathValue _get_addsub (const SMathData data, const CMathVariable value)
{
    return (data.char_start() == '+') ? 
        _get_addsub (data.create
        // start
        (get_muldiv (data.create (data.start + 1)).end + 1),     
        // value
        value + get_muldiv (data.create (data.start + 1)).value)
        
        : ((data.char_start() == '-') ? 
        _get_addsub (data.create
        // start
        (get_muldiv (data.create (data.start + 1)).end + 1),     
        // value
        value - get_muldiv (data.create (data.start + 1)).value)
        
        : SMathValue (value, data.start - 1));
}

Работа функций get_addsub и _get_addsub аналогична работе функций get_muldiv и _getmuldiv соответственно.

И наконец, осталось реализовать функцию solve:
constexpr CMathVariable solve (const char* str, const size_t size);
// get_value проверяет, что была прочитана вся строка
// (то есть, что value.end == size), и возвращает результат.
constexpr int get_value (const int size, const SMathValue value);

constexpr int solve (const char* str, const size_t size)
{
    return get_value (static_cast<int> (size), get_addsub (SMathData (str, static_cast<int> (size), 0)));
}

constexpr int get_value (const int size, const SMathValue value)
{
    return math_assert (value.end + 1 == size, "Digit or operator required"), value.value;
}

И последнее, что можно сделать: использовать свой класс чисел, в котором будут храниться числитель и знаменатель как отдельные переменные. Тут ничего особенного, просто все функции и конструктор имеют спецификатор constexpr.
Собственный класс чисел
class CMathVariable
{
private:

    int64_t numerator_;
    uint64_t denominator_;

    constexpr CMathVariable (int64_t numerator, uint64_t denominator);
    
    constexpr int64_t sign_ (int64_t a) const;
    constexpr uint64_t gcd_ (uint64_t a, uint64_t b) const;
    constexpr CMathVariable reduce_() const;

public:
    
    constexpr explicit CMathVariable (int number);
    
    constexpr CMathVariable operator + (const CMathVariable& n) const;
    constexpr CMathVariable operator - (const CMathVariable& n) const;
    constexpr CMathVariable operator * (const CMathVariable& n) const;
    constexpr CMathVariable operator / (const CMathVariable& n) const;

    constexpr int64_t numerator() const;
    constexpr uint64_t denominator() const;
    constexpr bool is_plus_inf() const;
    constexpr bool is_menus_inf() const;
    constexpr bool is_nan() const;
    constexpr bool is_inf() const;
    constexpr bool is_usual() const;
    constexpr bool is_integer() const;
    
    constexpr int to_int() const;
    constexpr int force_to_int() const;
    constexpr double to_double() const;
    
    friend constexpr CMathVariable operator - (const CMathVariable& n);
    friend constexpr CMathVariable operator + (const CMathVariable& n);
    friend std::ostream& operator << (std::ostream& os, const CMathVariable& var);
    
};

constexpr CMathVariable operator - (const CMathVariable& n);
constexpr CMathVariable operator + (const CMathVariable& n);
std::ostream& operator << (std::ostream& os, const CMathVariable& var);

constexpr CMathVariable::CMathVariable (int number) : 
    numerator_ (number), 
    denominator_ (1)
{
}

constexpr CMathVariable::CMathVariable (int64_t numerator, uint64_t denominator) : 
    numerator_ (numerator), 
    denominator_ (denominator)
{
}

constexpr int64_t CMathVariable::sign_ (int64_t a) const
{
    return (a > 0) - (a < 0);
}

constexpr uint64_t CMathVariable::gcd_ (uint64_t a, uint64_t b) const
{
    return (b == 0) ? a : gcd_ (b, a % b);
}

constexpr CMathVariable CMathVariable::reduce_() const
{
    return (numerator_ == 0) ? CMathVariable (0, sign_ (denominator_)) : 
        ((denominator_ == 0) ? CMathVariable (sign_ (numerator_), 0) : 
            CMathVariable (numerator_ / gcd_ (static_cast<uint64_t> (std::abs (numerator_)), denominator_), 
                           denominator_ / gcd_ (static_cast<uint64_t> (std::abs (numerator_)), denominator_)));
}

constexpr int64_t CMathVariable::numerator() const
{
	return numerator_;
}

constexpr uint64_t CMathVariable::denominator() const
{
	return denominator_;
}

constexpr bool CMathVariable::is_plus_inf() const
{
	return denominator_ == 0 && numerator_ > 0;
}

constexpr bool CMathVariable::is_menus_inf() const
{
    return denominator_ == 0 && numerator_ < 0;
}

constexpr bool CMathVariable::is_nan() const
{
    return denominator_ == 0 && numerator_ == 0;
}

constexpr bool CMathVariable::is_inf() const
{
    return denominator_ == 0 && numerator_ != 0;
}

constexpr bool CMathVariable::is_usual() const
{
    return denominator_ != 0;
}

constexpr bool CMathVariable::is_integer() const
{
    return denominator_ == 1;
}

constexpr int CMathVariable::to_int() const
{
    return static_cast<int> (numerator_ / denominator_);
}

constexpr int CMathVariable::force_to_int() const
{
    return (!(denominator_ == 1 && static_cast<int> (numerator_) == numerator_) ? 
           (throw std::logic_error ("CMathVariable can't be represented by int"), 0) : 0), to_int();
}

constexpr double CMathVariable::to_double() const
{
    return static_cast<double> (numerator_) / denominator_;
}

constexpr CMathVariable CMathVariable::operator + (const CMathVariable& n) const
{
    return CMathVariable (
        static_cast<int64_t> (n.denominator_ / gcd_ (denominator_, n.denominator_)) * numerator_ + 
	static_cast<int64_t> (denominator_ / gcd_ (denominator_, n.denominator_)) * n.numerator_, 
        
        denominator_ / gcd_ (denominator_, n.denominator_) * n.denominator_).reduce_();
}

constexpr CMathVariable CMathVariable::operator - (const CMathVariable& n) const
{
    return CMathVariable (
        static_cast<int64_t> (n.denominator_ / gcd_ (denominator_, n.denominator_)) * numerator_ - 
	static_cast<int64_t> (denominator_ / gcd_ (denominator_, n.denominator_)) * n.numerator_, 
        
        denominator_ / gcd_ (denominator_, n.denominator_) * n.denominator_).reduce_();
}

constexpr CMathVariable CMathVariable::operator * (const CMathVariable& n) const
{
    return CMathVariable (
            numerator_ * n.numerator_, 
            denominator_ * n.denominator_).reduce_();
}

constexpr CMathVariable CMathVariable::operator / (const CMathVariable& n) const
{
    return CMathVariable (
            numerator_ * static_cast<int64_t> (n.denominator_) * (n.numerator_ ? sign_ (n.numerator_) : 1), 
            denominator_ * static_cast<uint64_t> (std::abs (n.numerator_))).reduce_();
}

constexpr CMathVariable operator + (const CMathVariable& n)
{
    return n;
}

constexpr CMathVariable operator - (const CMathVariable& n)
{
    return CMathVariable (-n.numerator_, n.denominator_);
}

std::ostream& operator << (std::ostream& stream, const CMathVariable& var)
{
    if (var.is_plus_inf())
        stream << "+inf";
    else if (var.is_menus_inf())
        stream << "-inf";
    else if (var.is_nan())
        stream << "nan";
    else if (var.denominator() == 1)
        stream << var.numerator();
    else
        stream << var.numerator() << " / " << var.denominator();
    
    return stream;
}


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

Репозиторий с этой библиотекой находится тут: https://bitbucket.org/jjeka/mathcpp

Если есть какие-то недочеты или вопросы, пишите!
P.S. Считаю, что уже пора бы создавать хаб, посвященный C++11/14.
Поделиться публикацией
AdBlock похитил этот баннер, но баннеры не зубы — отрастут

Подробнее
Реклама

Комментарии 45

    +6
    Круто! А я вот CityHash 32/64/128-битный считал во время компиляции.
      0
      а как на счет md5/sha1? есть уже готовые?
      0
      Я немного отстал от жизни С++.

      Получается теперь компилятору можно сегфолт устроить?
      Что он будет в этом случае делать?
      Констекспры можно как-то дебажить?
        +1
        Сегфолт нельзя.
        constexpr char* s = "str";
        constexpr char* q = s + 10; // не скомпилируется
        

        Дебажить тоже нельзя. Но если вам требуется дебажить constexpr, то вы мсье. :)
          0
          constexpr char* s = "str";

          тоже уже не ок: warning: deprecated conversion from string constant to ‘char*’
          +5
          Компилятор тщательно следит за тем, что делают constexpr-функции. В случае деления на ноль, компилятор выдаст ошибку, показывая при этом стек вызовов.
          Еще в constexpr-функциях все переменные должны быть инициализированы, из-за чего не получится использовать неинициализированную переменную.
          Также компилятор ловит переполнение стека. В GCC по умолчанию глубина стека равна 512. В случае переполнения можно получить красивую ошибку из 512 строчек. С помощью флага можно расширить глубину стека.

          А дебажить constexpr затрудительно. Но это можно сделать, запустив функцию в runtime (например, передав в качестве параметра не constexpr-переменную).
          0
          error: constexpr variable cannot have non-literal type 'const std::regex' (aka 'const basic_regex<char>') constexpr std::regex pattern("(0-9)");
          Эх. Когда подобное будет возможно, без re2c — будет очень круто.
            +1
            Будем надеяться, что в C++14 или в C++17 добавят constexpr regex. Вообще после С++14 из-за того, что уберут много ограничений на constexpr-функции и классы, можно будет почти всю стандартную библиотеку переписать, используя constexpr.
              –1
              Зачем нужно переписывать стандартную библиотеку?
                +1
                Я имел ввиду, что разработчики компиляторов могут ее переписать (если это будет соотчествовать стандарту), тем самым увеличив производительность.
                  0
                  Переписывание стандартной библиотеки с мечтами об увеличении производительности не имеет смысла, потому что STL активно использует кучу. А с тем небольшим множеством функций, которое можно обернуть в constexpr (div, abs, max, min), компилятор может справиться сам. Переписывать стоит только лишь для возможности использовать эти функции внутри пользовательского кода с constexpr.

                  Перестаньте считать компилятор настолько тупым, что он не может свернуть вычисление констант самостоятельно =)
                    +2
                    На самом деле далеко не весь STL использует кучу. std::array или уже упомянутый std::regex — прямые кандидаты в переписывание на constexpr. Чтобы автомат для распознавания regexp'ов строился по время компиляции. Хотя надо посмотреть на интерфейсы — можно ли там будет обойтись без выделения памяти (с помощью шаблонов, к примеру).
              +7
              Я пойму, что у меня настал чёрный день, когда увижу эту строчку в выводе компиляции при сборке чужого проекта.
              +1
              Встроили-бы уже в язык нормальный интерпретатор и не мучались. И ключевое слово «interpret» для отделения того, что должно интепретироваться от того, что должно компилироваться.
                0
                constexpr как раз это и позволяет сделать (в C++11 не очень удачно). Но преимущество constexpr-функций в том, что в случае невозможности вызова функции на этапе компиляции, она запуститься в runtime, из-за чего не надо будет делать два экземпляра каждой функции.
                  0
                  а хотя бы warning в таком случае выдается, или он молча «глотает» ошибку?
                    +6
                    Я бы не назвал это ошибкой.
                    Если вы хотите получить гарантию того, что результат будет посчитан на этапе компиляции, достаточно приравнять результат функции constexpr-переменной.
                    constexpr int n = get_n(); // теперь в случае невозможности использовать constexpr-функцию будет выведена ошибка
                    
                      +2
                      Поведение аналогично этому:
                      #include <iostream>
                      
                      struct foo {
                          int bar;
                          foo(int bar) : bar(bar) {}
                          const int &getBar() {
                              return bar;
                          }
                      };
                      
                      int main () {
                          foo f(5);
                          int bar = f.getBar();
                          bar = 10;
                          std::cout << f.bar << " " << bar << std::endl; // 5 10
                      }
                      

                      Аналогично в том смысле, что если функция вернет константу — совсем не означает, что использовать результат функции можно только как константу.
                  +1
                  Странно это всё — ограничений много, код сложный, профит — небольшой. В конце-концов уже давно люди использовали подход с генераторами кода С++ с помощью других инструментов (moc, батники, скрипты на интерпретируемых языках), которые сгенерят тебе всё что угодно, без ограничений, с понятным исходным кодом и без затрат на рантайме.
                    0
                    Проблема с генераторами кода в том, что вход у них — нестандартный. Генератор-то его понимает, а вот как насчет вашей IDE, отладчика etc?
                    0
                    Что бы ни делать, лишь бы не вводить в язык нормальные синтаксические макросы.
                      +1
                      а где по вашему нормальные синтаксические макросы (кроме TH) имеют место быть?

                      (спрашиваю информации ради).
                        0
                        В большинстве диалектов LISP'а, как бы.
                          0
                          это верно, но я не стал упоминать, поскольку все диалекты лиспа динамически типизированы. А C++ типизирован статически (ну почти). Кроме того у лиспов есть уникальная, отличная от большинства языков, особенность — программа и данные записываются в одном и том же виде и имеют в корне одну и ту же структуру данных. Грех не изобрести понятие макроса.

                          Далеко не факт, что лисповская система макросов (и common lisp, и scheme) может быть построена над плюсами. Но и далеко не факт, что некое подобие Template Haskell тоже можно было бы построить над плюсами, потому что плюсы не функционально чисты.

                          Поэтому, хорошая система макросов в не функционально чистых статически типизированных языках — редкость. Разве что Nemerle стоит упомянуть.
                            0
                            >> Далеко не факт, что лисповская система макросов (и common lisp, и scheme) может быть построена над плюсами.

                            Лисповская система макросов может быть построена над любым языком, поскольку любую грамматику можно записать в виде s-выражений — нужно просто добавить преобразование туда и обратно.

                            На практике, конечно, неплохо еще иметь какой-нибудь сахар для обычный грамматики, с quotations и всем таким прочим. Но это именно сахар.
                              0
                              >> Лисповская система макросов может быть построена над любым языком, поскольку любую грамматику можно записать в виде s-выражений — нужно просто добавить преобразование туда и обратно.

                              после этого вы получите не с++, а лисп. Причем, довольно кривой раздутый диалект на котором разработчик будет писать только под пистолетом. Предлагаю вам переписать примеры из этой статьи в виде S-выражений (с правилами, которые вы сами придумаете), и убедиться что статически-типизированная семантика очень коряво ложится на S-выражения.

                              >> На практике, конечно, неплохо еще иметь какой-нибудь сахар для обычный грамматики, с quotations и всем таким прочим. Но это именно сахар.

                              макросы — это и есть частный случай синтаксического сахара. Их цель — сделать удобным написание и улучшить читабельность кода. Если вы попытаетесь записать какой-либо другой, отличный от лиспа язык, в S-выражения, вы, скорее всего (не всегда), и то и другое ухудшите. Поэкспериментируйте.

                              Поэтому лисповская система макросов чаще удобна только для лиспа. И именно поэтому, IMHO, в современном мире бизнес-логику в S-выражениях писать не принято, поскольку для этого почти всегда нужен талант и опыт, чтобы создать юзабельный и краткий DSL, а людей с такими качествами не много.
                                0
                                Не соглашусь. Макросы — это не синтаксический сахар. Точнее, один конкретно взятый макрос — это сахар, но сама концепция в целом — нет, т.к. без макросов эквивалентные вещи описываются только с повторениями. Так можно договориться до того, что функции тоже сахар (а что, их же можно руками поинлайнить).

                                То, что это будет неудобно на практике с s-выражениями в плюсах — это другой разговор.
                          0
                          Из того что я знаю — язык Nemerle, на rsdn.ru есть отличные статьи по этому поводу. Думаю, еще где-нибудь есть.
                          Хотя можно обобщить и сказать, что основная идея нормального метапрограммирования — написание «плагинов к компилятору» с использованием классической императивной (процедурной и объектно-ориентированной) парадигмы, причем неважно на том же языке что и основная программа, или на каком-то другом (возможно на упрощенном подмножестве основного языка, или на популярном скриптовом языке). Это не генерация текста программы, а именно работа с синтаксическими деревьями, поэтому компилятор должен предоставлять какое-то API для доступа к себе, а синтаксис основного языка обеспечивать прозрачное встраивание макросов и «квазицитирование» кода.
                        0
                        Интересно было бы посмотреть на реализацию парсер-комбинаторов, как boost::spirit, только на constexpr-ах.
                        Это как раз задача для функционального программирования.
                          +2
                          добро пожаловать под кат!

                          прочитал как «добро пожаловать в ад!»
                          и… не ошибся.
                            +1
                            В целом полезная статья, но бросились в глаза несколько неточностей/непонятностей.
                            constexpr int b = inc (a); // ошибка: преобразование int -> constexpr int

                            Это неверно. Ошибка в данном случае заключается не в том, что происходит попытка преобразования int в constexpr int, а в том, что a не является constexpr выражением, а такого типа, как constexpr int вообще не существует, так как constexpr не является частью типа, поэтому никаких попыток преобразования не делается.

                            То есть не получится создать исключительно constexpr-функцию, которая может работать только на этапе компиляции.

                            Вот это тоже непонятно.

                            constexpr int inc (int a)
                            {
                                return a + 1;
                            }
                            

                            является как раз такой функцией, если я не ошибаюсь. Что может заставить эту функцию отрабатывать не в компайл тайме (кроме конкретной реализации компилятора)?

                            Также constexpr-функции могут работать с объектами, это будет рассмотрено позже.

                            Непонятная фраза. Могут работать с объектами? А обычные не могут?
                              +2
                              Спасибо за конструктивный отзыв!

                              constexpr int b = inc (a); // ошибка: преобразование int -> constexpr int
                              Это неверно. Ошибка в данном случае заключается не в том, что происходит попытка преобразования int в constexpr int, а в том, что a не является constexpr выражением, а такого типа, как constexpr int вообще не существует, так как constexpr не является частью типа, поэтому никаких попыток преобразования не делается.

                              Тут вы правы, поправил статью.

                              То есть не получится создать исключительно constexpr-функцию, которая может работать только на этапе компиляции.

                              Вот это тоже непонятно.

                              Любую constexpr-функцию можно запустить в runtime, для этого достаточно передать аргумент, который нельзя посчитать на этапе компиляции. Напомню, что компилятор может посчитать на этапе компиляции выражения, которые содержат только литералы или constexpr-переменные и constexpr-функции.

                              Также constexpr-функции могут работать с объектами, это будет рассмотрено позже.

                              Непонятная фраза. Могут работать с объектами? А обычные не могут?

                              Возможно, я не очень понятно это написал. Имелось ввиду, что constexpr-функции могут работать не только с базовыми типами, но и с классами.
                                0
                                Любую constexpr-функцию можно запустить в runtime, для этого достаточно передать аргумент, который нельзя посчитать на этапе компиляции.


                                Я скорее имел в виду всю фразу целиком

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


                                Время вычисления функции зависит не от параметров (аргументов), а от того в каком контексте она используется. Например,

                                constexpr int foo(int i); // Здесь foo() может быть вычислена и в рантайме и в
                                const int j  = foo(2); // компайлтайме, несмотря, что её аргументом является constexpr
                                
                                constexpr int foo(int i);  // А здесь foo() обязаны быть вызвана в
                                constexpr int j  = foo(2); // компайлтайме
                                


                                Я к тому, что у вас не то, чтобы неправильно написано, а немного непонятно, не на то акцент сделан.
                                  0
                                  constexpr int foo(int i); // Здесь foo() может быть вычислена и в рантайме и в
                                  const int j  = foo(2); // компайлтайме, несмотря, что её аргументом является constexpr
                                  
                                  constexpr int foo(int i);  // А здесь foo() обязаны быть вызвана в
                                  constexpr int j  = foo(2); // компайлтайме
                                  

                                  В первом случае функция обязана быть вызвана в compile-time, несмотря на то, что ее значение приравнивается не constexpr-переменной, так как функция обязана вызвана в compile-time, если ее параметры можно посчитать на этапе компиляции.
                                  Бывают такие случаи, когда сложно уследить за тем, чтобы все параметры могли посчитаться на этапе компиляции, и как раз приравнивание значение гарантирует то, что функция будет вызвана в compile-time (в противном случае будет выведена ошибка).

                                  Так что время вычисления функции зависит только от параметров.
                              –2
                              ===
                              return (y == 0)? throw std::logic_error («x can't be zero»): (y / x);

                              В случае вызова функции во время компиляции мы увидим ошибку, что конструкция throw не может находиться в constexpr-функции, а в runtime функция выбросит
                              ===

                              Если ошибка выявляется на этапе компиляции, то о каком runtime-е речь? Кроме того, тут ещё ошибка в том, что типы второго и третьего операндов тернарного оператора должны быть одинаковыми либо приводиться друг к другу.

                              И если уж совсем придираться, то в условии должно проверяться х==0, а лучше даже 0==x ;)
                                0
                                В случае запуска этой функции в runtime (это возможно сделать, передав в функцию параметр, который нельзя посчитать на этапе компиляции), будет выброшено исключение.

                                Вообще GCC выполняет код по мере необходимости (по стандарту он может проверять весь constexpr-код сразу, но он этого не делает). Так что если y не будет равняться нулю, то кусок кода после '?' будет проигнорирован. Но чтобы вопросов не было, можно это переписать таким образом:
                                return (y == 0)? (throw std::logic_error («x can't be zero»), 0): (y / x);
                                
                                  –1
                                  а почему не использовать static_assert вместо throw?
                                    +1
                                    Цитата из статьи:
                                    Когда пишешь какую-то constexpr-функцию, которую потом будут часто использовать, хорошо бы возвращать читабельную ошибку. Тут можно ошибочно предположить, что static_assert как раз для этого подходит. Но static_assert использовать не получится, так как параметры функций не могут быть constexpr, из-за чего значения параметров не гарантированно будут известны на этапе компиляции.
                                      0
                                      Это я понял, так пусть себе теряется constrexpr для ошибки (конвертация contrexpr int -> int).
                                        0
                                        Это не будет работать, так как условие внутри static_assert не гарантировано будет известно на этапе компиляции, из-за чего будет ошибка компиляции в любом случае, даже если ассерт не должен сработать.
                                  0
                                  Так и не понял, зачем нагромождать язык миллионом новых ключевых слов. Неужели компилятор сам не может понять, какую функцию можно выполнить на этапе компиляции, а какую нет
                                    0
                                    Например, чтобы указать компилятору, что данная функция (не помеченная как constexpr) не может вычислять значение в constexpr-контексте.
                                      0
                                      То есть для того, чтобы паре функций, которые мы почему-то не хотим разрешать в constexpr-контексте, было запрещено это делать — мы будем теперь навешивать аттрибут на тысячи всех остальных функций?

                                      Нет, мы будем, конечно, куда деваться — но назвать это чем-то, кроме как ошибкой в дизайне языка сложно…
                                        0
                                        Начиная с C++14 constexpr может работать и в не константном режиме. Но обратно не работает, такой дизайн. Программист ведь имеет право не обозначать функцию constexpr, например в тех случаях когда он явно знает, что она не является чистой.
                                          0
                                          Начиная с C++14 constexpr может работать и в не константном режиме.
                                          Начиная с C++11.

                                          Но обратно не работает, такой дизайн.
                                          Такая ошибка в дизайне. Очень большая, серьёзная и неприятная.

                                          Программист ведь имеет право не обозначать функцию constexpr, например в тех случаях когда он явно знает, что она не является чистой.
                                          Однако он обязан это сделать в тех случаях, когда он хочет, чтобы её можно было использовать в constexpr-выражениях. Однако зачастую — не может. Ибо в разных версиях C++ требования к constexpr-функциям разные и вы не можете пометить функцию как constexpr и сказав в документации «на старых C++ комиляторах эту функцию нельзя использовать в constexpr-выражениях». Вам приходится либо писать «для использования этой библиотеки вам потребуется C++17 компилятор», либо обмазывать всё ваше творение кучей #ifdefов.

                                          Что радикально отличается от случая когда бы помечались бы лишь функции, которые нельзя использовать в constexpr-выражениях!

                                          Заметьте, кстати, что тот факт, что у вас функция помечена как constexpr нифига не значит, что её можно будет использовать в constexpr-выражениях!

                                          Вот, например, такая функция:
                                          constexpr int badfunc(bool to_be) {
                                              if (to_be or not to_be) {
                                                  printf("Yeah, we are here!\n");
                                              }
                                              return 0;
                                          }
                                          И как вы собрались применять её?

                                          Извините, но constexpr спецификатор в применении к функции — это 100% ошибка дизайна. Очень глупая. Хотя во времена C++11 она казалась разумной — но дальнейшее развитие языка показало, что это ни разу не так.

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

                                  Самое читаемое