Одна из новых возможностей C++11 — спецификатор
В первой части будет рассказано про
С помощью нее можно будет написать следующий код:
И ответ в виде дроби будет получен на этапе компиляции:
Сразу предупреждаю, код этой библиотеки сложно понять.
Кому эта тема интересна, добро пожаловать под кат!
Сначала пара слов о том, что вообще такое спецификатор
Ключевое слово
Ключевое слово в данном случае означает создание константы. Причем expression должно быть известно на этапе компиляции.
Рассмотрим такой пример:
В случае «утери»
Параметры функций не могут быть
Также
GCC, начиная с версии 4.4, поддерживает
Теперь, когда вы поняли, как это все удобно, можно добавить ложку дегтя в бочку меда. Причем довольно большую ложку.
Начнем с ограничений
Тут вроде ничего необычного. Основные ограничения наложены на constexpr-функции:
* С C++14
На
Все нестатические члены класса и члены базовых классов должны быть инициализированы каким-либо образом (в конструкторе, используя списки иницилизации или иницилизацией членов класса при объявлении), причем присвоенные им выражения должны содержать только литералы или
Получается, что в функциях нельзя инициализировать переменные, создавать циклы и конструкции
Конечно, все равно все эти возможности можно реализовать. Вместо циклов использовать рекурсию, вместо конструкции
Все это сильно напоминает функциональное программирование. В функциональных языках программирования, как правило, также нельзя заводить переменные и отсутствуют циклы. Действительно, функции вызывать можно, функции высших порядков тоже можно реализовать, используя указатели на функции (к сожалению, анонимные функции (лямбды) нельзя использовать в
Но тут у C++ большие проблемы с синтаксисом: анонимные функции нельзя использовать, все действия функции являются одним длинным выражением, а с добавлением оператора «
Но на этом проблемы не заканчиваются. Когда пишешь какую-то
Как же выводить ошибки? Единственный более-менее нормальный способ, который я нашел, заключается в выбрасывании исключения:
В случае вызова функции во время компиляции мы увидим ошибку, что конструкция
Ошибку сложно будет найти, но хоть что-то.
Такой способ вывода ошибки еще не соответствует стандарту языка, ничего не запрещает компилятору всегда выдавать ошибку о том, что нельзя использовать
В итоге сложно писать, сложно отлаживать, сложно понимать такие конструкции.
Но все не так плохо, в C++14 очень многие ограничения уберут.
Как уже было сказано, в новом стандарте
Второе незначительное изменение заключается в том, что теперь
В C++11 следующие строчки были равносильными, а в С++14 это уже не так:
Объяснение этому можно найти тут.
И наконец, главное изменение разрешает использовать почти любые конструкции в
Теперь тело
А тело
В итоге после появления компиляторов, которые поддерживают C++14, можно будет писать constexpr-функции, которые почти ничем не будут отличаться от обычных. А пока приходится писать довольно запутанный код.
В качестве примера использования constexpr будет приведена библиотека, которая будет считать результат математического выражения, находящегося в строке.
Итак, мы хотим, чтобы можно было писать такой код:
Тут используется еще одна новая возможность C++11: пользовательские литералы. В данном случае они хороши тем, что функция гарантированно будет вызвана на этапе компиляции, даже если получившееся значение будет присвоено не
Объявляется пользовательский литерал таким образом:
В качестве ассерта будет использоваться следующий макрос:
Библиотека может складывать, вычитать, умножать, делить, возводить в степень, также есть поддержка скобок. Реализовано это будет с помощью рекурсивного спуска.
Приоритеты операторов будут такие (от более высоких к более низким):
Функции считывания числа, степени, суммы и так далее будут принимать один параметр: структуру
А возвращать эти функции будут структуру
Для считывания числа будут 3 функции (одна основная и две вспомогательных):
Вот такая запутанная конструкция получается.
Работа со скобками:
Если на текущем индексе число, то функция вызывает
Дальше идет функция возведения в степень:
В функции
Получается, что строка
Произведение и деление обрабатываются в одной функции:
Довольно сложно понять эту конструкцию, писать ее также затруднительно. Тут идет проверка, является ли текущий символ
Аналогично происходит с суммой и разностью:
И наконец, осталось реализовать функцию
И последнее, что можно сделать: использовать свой класс чисел, в котором будут храниться числитель и знаменатель как отдельные переменные. Тут ничего особенного, просто все функции и конструктор имеют спецификатор
После этого надо немного изменить код рекурсивного спуска и в итоге получить требуемое.
Написание рекурсивного спуска на constexpr-функциях заняло где-то день, хотя обычный рекурсивный спуск без проблем пишется за час. Проблемы были с путаницей со скобками, со сложностью отладки, с непонятными ошибками, с непродуманностью архитектуры (да, теперь даже для рекурсивного спуска надо тщательно все продумывать).
Репозиторий с этой библиотекой находится тут: https://bitbucket.org/jjeka/mathcpp
Если есть какие-то недочеты или вопросы, пишите!
P.S. Считаю, что уже пора бы создавать хаб, посвященный C++11/14.
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_asserttypedefили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»)
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))
Библиотека может складывать, вычитать, умножать, делить, возводить в степень, также есть поддержка скобок. Реализовано это будет с помощью рекурсивного спуска.
Приоритеты операторов будут такие (от более высоких к более низким):
- Сложение и вычитание
- Умножение и деление
- Возведение в целую степень
Функции считывания числа, степени, суммы и так далее будут принимать один параметр: структуру
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.
