Pemrogram C ++ tahu betul (mudah-mudahan!) Bahwa berbagai macam perhitungan dapat dilakukan pada waktu kompilasi. Andai saja kalkulasi ini "bersih", tanpa efek samping. Ini dilakukan dalam template dan fungsi constexpr.
Ungkapan murni berarti tidak peduli berapa kali kita mencoba untuk mengevaluasinya, kita akan mendapatkan hasil yang sama. Oleh karena itu, demi alasan efisiensi, disarankan untuk menghafal hasilnya agar tidak melakukan pekerjaan yang sama untuk kedua kalinya. Tetapi seberapa baik kompiler melakukannya?
Untuk pengujian stres, mari kita ambil rumus Fibonacci yang naif:
f(n) = (n < 2) ? 1 : f(n-1) + f(n-2)
Kami tertarik karena beberapa alasan:
kedalaman rekursi secara linier tergantung pada n
tanpa memoisasi, ia mereduksi menjadi penjumlahan dari f (n), dan ini, ingat, adalah eksponen di dasar rasio emas
dengan memoization - untuk menghafal nilai n
Bagaimana cara menghitung rumus ini pada waktu kompilasi?
Ada 3 teknik untuk ini.
Yang pertama, terkenal sejak lama (sejak C ++ 98): template dengan parameter integer dan anggota konstan. (Di zaman kuno, enum digunakan, kemudian konstanta statis muncul).
// ,
template<unsigned n> struct int_ { static constexpr unsigned value = n; };
template<unsigned n> struct f;
template<> struct f<0> : int_<1> {};
template<> struct f<1> : int_<1> {};
template<unsigned n> struct f : int_< f<n-1>::value + f<n-2>::value > {};
template<unsigned n> constexpr unsigned F = f<n>::value;
(kami akan menggunakan unsigned, karena kami tidak memerlukan nilai sebenarnya dari angka tersebut untuk eksperimen, dan kami tidak ingin mengalami overflow integer).
Teknik kedua tersedia di C ++ 11: fungsi constexpr.
constexpr unsigned f(unsigned n) {
return n < 2 ? f(n-1) + f(n-2);
}
template<unsigned n> constexpr unsigned F = f(n);
, . : . , , ( ).
constexpr unsigned f(unsigned n) {
unsigned a = 1, b = 1;
for (i = 1; i < n; ++i) {
unsigned c = a + b;
a = b; b = c;
}
return b;
}
.
— , — expression template. , . , , expression template (, ). .
// ,
template<unsigned n> struct int_ { static constexpr unsigned value = n; };
// expression template, :
template<unsigned x, unsigned y> auto operator + (int_<x>, int_<y>) {
return int_<x+y>{};
}
template<unsigned n> auto f(int_<n> /*arg*/) {
if constexpr (n < 2) {
return int_<1>{};
} else {
// (expression template !)
return f(int_<n-1>{}) + f(int_<n-2>{});
// - , ,
// :
return decltype( f(int_<n-1>{}) + f(int_<n-2>{}) ){};
// :
using t1 = decltype(f(int_<n-1>{}));
using t2 = decltype(f(int_<n-2>{}));
return int_<t1::value + t2::value>{};
}
}
template<unsigned n> constexpr unsigned F = decltype(f(int_<n>{}))::value;
, : / , if constexpr
, C++17. . ( : , ; , , ).
: constexpr. .
, , .
(instantiate) n, n-1 n-2, , , n-2 n-3, 1 0, 2 1 ( ), 3 2 ( ), . , .
, — .
gcc -ftemplate-depth
900, clang -ftemplate-backtrace-limit
— 1024.
— . ? , : . expression template .
, constexpr . , : gcc -fconxtexpr-depth
, clang — -fconstexpr-backtrace-limit
, 512.
, .
gcc 9.3 ! F<512>
F<1022>
, .
, 10.1, gcc . -fconstexpr-ops-limit
, 33554432.
?
F<512>
F<1022>
— ? -? , .
template<unsigned n> struct f;
template<unsigned n> struct g;
template<> struct f<0> : int_<1> {};
template<> struct f<1> : int_<1> {};
template<unsigned n> struct f : int_< f<n-1>::value + f<n-2>::value > {};
template<> struct g<0> : int_<1> {};
template<> struct g<1> : int_<1> {};
template<unsigned n> struct g : int_< g<n-2>::value + g<n-1>::value > {};
template<unsigned n> constexpr unsigned F = f<n>::value;
template<unsigned n> constexpr unsigned G = g<n>::value;
1, — 2. , , , .
expression template.
GodBolt
,
-
clang 11.0.0 — expression template
clang 9.0.0 — expression template, , : -
gcc 9.3 — !
:
|
|
|
gcc 9.3 |
gcc 10.1 |
clang 11.0.0 |
class template
|
CT::F |
899 |
899 |
1024 |
CT::G |
1798 |
1798 |
2048 |
|
expression template
|
ET::F |
899 |
899 |
702 |
ET::G |
1796 |
1796 |
1402 |
|
constexpr
|
CE::F |
512 |
35 |
26 |
CE::G |
1022 |
41 |
26 |
.
#include <iostream>
template<unsigned n> struct int_ { static constexpr unsigned value = n; };
template<unsigned x, unsigned y> auto operator + (int_<x>, int_<y>) {
return int_<x+y>{};
}
namespace CT { // class template
template<unsigned n> struct f;
template<> struct f<0> : int_<1> {};
template<> struct f<1> : int_<1> {};
template<unsigned n> struct f : int_<f<n-1>::value + f<n-2>::value> {};
template<unsigned n> struct g;
template<> struct g<0> : int_<1> {};
template<> struct g<1> : int_<1> {};
template<unsigned n> struct g : int_<g<n-2>::value + g<n-1>::value> {};
template<unsigned n> constexpr unsigned F = f<n>::value;
template<unsigned n> constexpr unsigned G = g<n>::value;
} // namespace CT
namespace ET { // expression template
template<unsigned n> auto f(int_<n>) {
if constexpr (n < 2) {
return int_<1>{};
} else {
return f(int_<n-1>{}) + f(int_<n-2>{});
}
}
template<unsigned n> auto g(int_<n>) {
if constexpr (n < 2) {
return int_<1>{};
} else {
return g(int_<n-2>{}) + g(int_<n-1>{});
}
}
template<unsigned n> constexpr unsigned F = decltype(f(int_<n>{}))::value;
template<unsigned n> constexpr unsigned G = decltype(g(int_<n>{}))::value;
} // namespace ET
namespace CE { // constexpr
constexpr unsigned f(unsigned n) {
return n < 2 ? 1 : f(n-1) + f(n-2);
}
constexpr unsigned g(unsigned n) {
return n < 2 ? 1 : g(n-2) + g(n-1);
}
template<unsigned n> constexpr unsigned F = f(n);
template<unsigned n> constexpr unsigned G = g(n);
} // namespace CE
int main() {
std::cout << CT::F<899> << std::endl;
std::cout << CT::G<1798> << std::endl;
std::cout << ET::F<899> << std::endl;
std::cout << ET::G<1796> << std::endl;
std::cout << CE::F<35> << std::endl;
std::cout << CE::G<35> << std::endl;
}
?
.
. , , constexpr' — -, , . , , .
. , .
" "
— , , , — , constexpr-, ... .
Fungsi yang menghitung jumlah operasi untuk menghitung angka Fibonacci
f(n, t) = n < 2 ? t+1 : f(n-1, f(n-2, t))
f(n) = f(n, 0)
Template ekspresi akan terlihat seperti ini.
template<unsigned n, unsigned t>
auto f(int_<n>, int_<t>) {
if constexpr (n < 2) {
return int_<t+1>{};
} else {
return f(int_<n-1>{}, f(int_<n-2>{}, int_<t>{}));
}
}
int main() {
std::cout << decltype(f(int_<30>{}, int_<0>{}))::value << std::endl;
}
Selama 26 - dentang bekerja selama sekitar setengah menit. Untuk usia 30, dia melahap lebih dari 8 gigabyte memori dan membawa laptop saya ke swap, setelah itu eksperimen harus dihentikan.