Доброе утро!
Как-то раз мне захотелось научиться вычислять значение функций во время компиляции исходных кодов на C++. Сегодня я расскажу, что из этого получилось.
Вычисления во время компиляции — давно известная тема. Для этого в C++ есть шаблоны, а с C++11 всё становится ещё проще. Приступим.
Для начала попробуем реализовать вычисление чисел Фибоначчи. Напомню, числа Фибоначчи — это следующая последовательность:
f0 = f1 = 1, fn = fn-1 + fn-2.
Как написано — так и сделаем!
Определим шаблонную структуру, принимающую индекс числа и рекуррентно вычисляющую новое:
using xy_type = std::enable_if<std::is_integral<unsigned decltype(N)>::value,
unsigned decltype(N)>::type;
template <xy_type n = N>
struct fib {
constexpr static xy_type f = fib<n-1>().f + fib<n-2>().f;
};
Здесь всё довольно просто: в структуре есть поле, значение которого — сумма этого же поля из таких же структур, но с другим шаблонным аргументом.
Определим первые два значения, чтобы рекурсия закончилась:
template <>
struct fib<0> {
constexpr static xy_type f = 1;
};
template <>
struct fib<1> {
constexpr static xy_type f = 1;
};
Ну и посчитаем:
constexpr static xy_type f = fib<N>().f;
int main() {
std::cout << f << std::endl;
return 0;
}
Кроме того, добавим вывод ошибки, если при компиляции указано некорректное N:
#define STR(X) #X
#define DEFER(M,...) M(__VA_ARGS__)
#define CUSTOM_ERROR_(a) _Pragma(STR(a))
#define CUSTOM_ERROR(...) CUSTOM_ERROR_(GCC error DEFER(STR, __VA_ARGS__))
#ifndef N
CUSTOM_ERROR(usage: c++ --std=c++11 -fmax-errors=1 -DN=<argument> __FILE__)
#endif // N
Запустим то, что у нас получилось:
$ g++ ./fibonacci_valid.cpp
./fibonacci_valid.cpp:10:11: error: usage: c++ --std=c++11 -fmax-errors=1 -DN=<argument> "./fibonacci_valid.cpp"
CUSTOM_ERROR(usage: c++ --std=c++11 -fmax-errors=1 -DN=<argument> __FILE__)
^~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
./fibonacci_valid.cpp:13:67: error: ‘N’ was not declared in this scope
using xy_type = std::enable_if<std::is_integral<unsigned decltype(N)>::value,
...skipping...
$ g++ -DN=abc ./fibonacci_valid.cpp
<command-line>:0:3: error: ‘abc’ was not declared in this scope
./fibonacci_valid.cpp:13:67: note: in expansion of macro ‘N’
using xy_type = std::enable_if<std::is_integral<unsigned decltype(N)>::value,
...skipping...
$ g++ -DN=5 ./fibonacci_valid.cpp
$ ./a.out
8
$ g++ -DN=1 ./fibonacci_valid.cpp
$ ./a.out
1
$ g++ -DN=20 ./fibonacci_valid.cpp
$ ./a.out
10946
Отлично, мы увидели, что есть реакция на отсутствие аргумента (компилятор услужливо подчеркнул usage), есть реакция на строки вместо чисел, и успешная компиляция и какие-то расчёты при корректно заданном N.
Но это скучно. Давайте и ответ выведем во время компиляции. Изменения каснутся лишь последней части. Создадим удалённую шаблонную функцию и попробуем ею воспользоваться:
template <xy_type answer>
bool deleted() = delete;
static bool n = deleted<f>();
int main() {
return 0;
}
Запустим компиляцию:
$ g++ -DN=10 ./fibonacci.cpp
./fibonacci.cpp:36:28: error: use of deleted function ‘bool deleted() [with int answer = 89]’
static bool n = deleted<f>();
^
./fibonacci.cpp:34:6: note: declared here
bool deleted() = delete;
^~~~~~~
$ clang++ -std=c++11 -DN=10 ./fibonacci.cpp
./fibonacci.cpp:36:17: error: call to deleted function 'deleted'
static bool n = deleted<f>();
^~~~~~~~~~
./fibonacci.cpp:34:6: note: candidate function [with answer = 89] has been explicitly deleted
bool deleted() = delete;
^
1 error generated.
Видим теперь ошибку компиляции, содержащую строку вида with answer = XXX. Это то, чего мы и добивались, получив answer благодаря удачно подобранному имени шаблонного аргумента удалённой функции. Ну и, конечно, пользуемся тем, как clang и gcc одинаково выводят подобную ошибку, демонстрируя значение шаблонного параметра.
На всякий случай приведу полный код:
#include <iostream>
#include <type_traits>
#define STR(X) #X
#define DEFER(M,...) M(__VA_ARGS__)
#define CUSTOM_ERROR_(a) _Pragma(STR(a))
#define CUSTOM_ERROR(...) CUSTOM_ERROR_(GCC error DEFER(STR, __VA_ARGS__))
#ifndef N
CUSTOM_ERROR(usage: c++ --std=c++11 -fmax-errors=1 -DN=<argument> __FILE__)
#endif // N
using xy_type = std::enable_if<std::is_integral<decltype(N)>::value,
decltype(N)>::type;
template <xy_type n = N>
struct fib {
constexpr static xy_type f = fib<n-1>().f + fib<n-2>().f;
};
template <>
struct fib<0> {
constexpr static xy_type f = 1;
};
template <>
struct fib<1> {
constexpr static xy_type f = 1;
};
constexpr static xy_type f = fib<N>().f;
template <xy_type answer>
bool deleted() = delete;
static bool n = deleted<f>();
int main() {
return 0;
}
В качестве бонуса, проведём эксперимент:
$ g++ -DN=10000000 ./fibonacci.cpp
./fibonacci.cpp: In instantiation of ‘constexpr const xy_type fib<9999101>::f’:
./fibonacci.cpp:18:43: recursively required from ‘constexpr const xy_type fib<9999999>::f’
./fibonacci.cpp:18:43: required from ‘constexpr const xy_type fib<10000000>::f’
./fibonacci.cpp:31:39: required from here
./fibonacci.cpp:18:32: fatal error: template instantiation depth exceeds maximum of 900 (use -ftemplate-depth= to increase the maximum)
constexpr static xy_type f = fib<n-1>().f + fib<n-2>().f;
^~~~~~~~~~
compilation terminated.
$ g++ -ftemplate-depth=100000000 -DN=10000000 ./fibonacci.cpp
g++: internal compiler error: Segmentation fault (program cc1plus)
Please submit a full bug report,
with preprocessed source if appropriate.
See <file:///usr/share/doc/gcc-7/README.Bugs> for instructions.
$ clang++ -std=c++11 -ftemplate-depth=100000000 -DN=10000000 ./fibonacci.cpp
#0 0x00007f0ebe18e155 llvm::sys::PrintStackTrace(llvm::raw_ostream&) (/usr/lib/llvm-4.0/bin/../lib/libLLVM-4.0.so.1+0x780155)
#1 0x00007f0ebe18c3b6 llvm::sys::RunSignalHandlers() (/usr/lib/llvm-4.0/bin/../lib/libLLVM-4.0.so.1+0x77e3b6)
#2 0x00007f0ebe18c4d3 (/usr/lib/llvm-4.0/bin/../lib/libLLVM-4.0.so.1+0x77e4d3)
...skipping...
Stack dump:
0. Program arguments: /usr/lib/llvm-4.0/bin/clang -cc1 -triple x86_64-pc-linux-gnu -emit-obj -mrelax-all -disable-free -disable-llvm-verifier -discard-value-names -main-file-name fibonacci.cpp -mrelocation-model static -mthread-model posix -mdisable-fp-elim -fmath-errno -masm-verbose -mconstructor-aliases -munwind-tables -fuse-init-array -target-cpu x86-64 -dwarf-column-info -debugger-tuning=gdb -resource-dir /usr/lib/llvm-4.0/bin/../lib/clang/4.0.1 -D N=10000000 -internal-isystem /usr/bin/../lib/gcc/x86_64-linux-gnu/7.3.0/../../../../include/c++/7.3.0 -internal-isystem /usr/bin/../lib/gcc/x86_64-linux-gnu/7.3.0/../../../../include/x86_64-linux-gnu/c++/7.3.0 -internal-isystem /usr/bin/../lib/gcc/x86_64-linux-gnu/7.3.0/../../../../include/x86_64-linux-gnu/c++/7.3.0 -internal-isystem /usr/bin/../lib/gcc/x86_64-linux-gnu/7.3.0/../../../../include/c++/7.3.0/backward -internal-isystem /usr/include/clang/4.0.1/include/ -internal-isystem /usr/local/include -internal-isystem /usr/lib/llvm-4.0/bin/../lib/clang/4.0.1/include -internal-externc-isystem /usr/include/x86_64-linux-gnu -internal-externc-isystem /include -internal-externc-isystem /usr/include -std=c++11 -fdeprecated-macro -fdebug-compilation-dir /home/valeriyk/doc/tpl -ftemplate-depth 100000000 -ferror-limit 19 -fmessage-length 227 -fobjc-runtime=gcc -fcxx-exceptions -fexceptions -fdiagnostics-show-option -fcolor-diagnostics -o /tmp/fibonacci-eb891d.o -x c++ ./fibonacci.cpp
1. ./fibonacci.cpp:31:38: current parser token '.'
2. ./fibonacci.cpp:17:8: instantiating class definition 'fib<10000000>'
3. ./fibonacci.cpp:17:8: instantiating class definition 'fib<9999999>'
4. ./fibonacci.cpp:17:8: instantiating class definition 'fib<9999998>'
5. ./fibonacci.cpp:17:8: instantiating class definition 'fib<9999997>'
...skipping...
740. ./fibonacci.cpp:17:8: instantiating class definition 'fib<9999262>'
741. ./fibonacci.cpp:17:8: instantiating class definition 'fib<9999261>'
clang: error: unable to execute command: Segmentation fault
clang: error: clang frontend command failed due to signal (use -v to see invocation)
clang version 4.0.1-10+b1 (tags/RELEASE_401/final)
Target: x86_64-pc-linux-gnu
Thread model: posix
InstalledDir: /usr/bin
clang: note: diagnostic msg: PLEASE submit a bug report to http://llvm.org/bugs/ and include the crash backtrace, preprocessed source, and associated run script.
clang: note: diagnostic msg:
********************
PLEASE ATTACH THE FOLLOWING FILES TO THE BUG REPORT:
Preprocessed source(s) and associated run script(s) are located at:
clang: note: diagnostic msg: /tmp/fibonacci-548661.cpp
clang: note: diagnostic msg: /tmp/fibonacci-548661.sh
clang: note: diagnostic msg:
********************
Вот мы и ссегфолтили компиляторы! Те, кто любят пожёстче, могут написать ulimit -s unlimited
перед запуском.
Если тема статьи понравилась, то в качестве упражнения читателю предлагается сделать реализацию ещё чего-нибудь, к примеру — факториала.