Constexpr Applications - Scott Schurr
Constexpr Applications - Scott Schurr
void free_func () {
constexpr float pi = 3.14159265; // Object definition
static_assert ((3.1 < pi) && (pi < 3.2), "Yipe!");
}
struct my_struct {
// Static data member of literal type
static constexpr char who[] = "Gabriel Dos Reis";
static_assert (who[0] == 'G', "Yipe!");
static constexpr const char* a = &who[1];
static_assert (*a == 'a', "Yipe!");
};
4
constexpr Computations
• constexpr declaration allowed on:
• Free functions
• Member functions
• Constructors
• Allowed code:
• Constrained in C++11
• Relaxed somewhat in C++14
• constexpr constructor allows user-
defined literal types
5
Topics
• Compile-time parsing
• Compile-time containers
• Compile-time floating point
• Future and Summary
6
Compile-time Parsing
7
C++11 binary literal
8
constexpr11_bin
template <typename T = std::uint32_t>
constexpr T constexpr11_bin(
const char* t,
std::size_t b = 0, // bit count
T x = 0) // accumulator
{
return
*t == '\0' ? x : // end recursion
b >= std::numeric_limits<T>::digits ?
throw std::overflow_error("Too many bits!") :
*t == ',' ? constexpr11_bin<T>(t+1, b, x) :
*t == '0' ? constexpr11_bin<T>(t+1, b+1, (x*2)+0) :
*t == '1' ? constexpr11_bin<T>(t+1, b+1, (x*2)+1) :
throw std::domain_error(
"Only '0', '1', and ',' may be used");
}
9
Using constexpr11_bin
int main()
{
using u8_t = std::uint8_t;
static_assert(
maskA + maskB + maskC + maskD == 0xFF, "Yipe!");
return 0;
}
10
C++14 binary literal
11
constexpr14_bin
template <typename T = std::uint32_t>
constexpr T constexpr14_bin(const char* t)
{
T x = 0;
std::size_t b = 0;
for (std::size_t i = 0; t[i] != '\0'; ++i) {
if (b >= std::numeric_limits<T>::digits)
throw std::overflow_error("Too many bits!");
switch (t[i]) {
case ',': break;
case '0': x = (x*2); ++b; break;
case '1': x = (x*2)+1; ++b; break;
default: throw std::domain_error(
"Only '0', '1', and ',' may be used");
}
}
return x;
}
12
More constexpr Questions
13
constexpr Limits
Minimum recommended
implementation quantities [Annex B]:
• Recursive constexpr function
invocations: 512
• Full-expressions evaluated within a
core constant expression: 1,048,576
Actual limits are up to your compiler(s)
14
User Errors at Compile Time
int main()
{
constexpr auto mask =
constexpr11_bin<std::uint8_t>("1110 0000");
static_assert(mask == 0xE0, "Yipe!");
return 0;
}
error: constexpr variable 'mask' must be initialized by a
constant expression
constexpr auto mask =
^
note: subexpression not valid in a constant expression
throw std::domain_error(
^
note: in call to 'constexpr11_bin({&"1110 0000"[0],
9}, 4, 4, 14)'
t[i] == '0' ? constexpr11_bin<T>(t, i+1, b+1, (x*2)+0) :
^
15
User Errors At Runtime
int main()
{
auto mask = // <- Not constexpr!
constexpr11_bin<std::uint8_t>("1110 0000");
assert(mask == 0xE0);
return 0;
}
16
Runtime Execution?
• Really handy for debugging
• In this case not so good for users
• Little reason for runtime conversion
• Which one causes a runtime error?
constexpr auto maskA =
constexpr11_bin<std::uint8_t>("1110 0000");
auto maskB =
constexpr11_bin<std::uint8_t>("0001 1111");
17
Guidance
Make interfaces easy to use correctly
and hard to use incorrectly.
Item 18 Effective C++ Third Edition by Scott Meyers
18
A Way to Force Compile-Time Only?
19
Unresolved Symbol In Throw
extern const char* compile11_bin_invoked_at_runtime;
template <typename T = std::uint32_t>
constexpr T compile11_bin(
constexpr_txt t,
std::size_t i = 0, // index
std::size_t b = 0, // bit count
T x = 0) // accumulator
{
return
i >= t.size() ? x : // end recursion
b >= std::numeric_limits<T>::digits ?
throw std::overflow_error("Too many bits!") :
t[i] == ',' ? compile11_bin<T>(t, i+1, b, x) :
t[i] == '0' ? compile11_bin<T>(t, i+1, b+1, (x*2)+0) :
t[i] == '1' ? compile11_bin<T>(t, i+1, b+1, (x*2)+1) :
throw std::domain_error( // Only '0', '1', and ','
compile11_bin_invoked_at_runtime);
}
20
User Errors at Link-Time
int main()
{
auto mask = // <- Not constexpr!
compile11_bin<std::uint8_t>("1110 0000");
assert(mask == 0xE0);
return 0;
}
21
Why Does It Work?
22
Is This The Best You Can Do?
• Error is ugly
• Doesn’t identify line that caused error
• May not work:
24
Compile-Time Parsing Summary
• Interesting technique
• Limited applicability
25
Topics
• Compile-time parsing
• Compile-time containers
• Compile-time floating point
• Future and Summary
26
Compile-Time Containers
27
Motivation
28
First constexpr Array
int main()
{
constexpr int ramp_len = 10;
constexpr float ramp[ramp_len] {
0.0f,0.1f,0.2f,0.3f,0.4f,0.5f,0.6f,0.7f,0.8f,0.9f
};
for (int ramps = 0; ramps < 10; ++ramps) {
for (int i = 0; i < ramp_len; ++i) {
// Moving average filter
float avg = ramp[i == 0 ? ramp_len - 1 : i];
avg += ramp[i];
avg += ramp[i == ramp_len - 1 ? 0 : i];
avg /= 3;
std::cout << avg << std::endl;
}
}
return 0;
}
29
More Interesting Array
constexpr float pi = pi_num<float>();
constexpr std::size_t sine_size = 10;
constexpr float sine[sine_size] {
constexpr_sin(0.0f * 2 * pi),
constexpr_sin(0.1f * 2 * pi),
constexpr_sin(0.2f * 2 * pi),
constexpr_sin(0.3f * 2 * pi),
constexpr_sin(0.4f * 2 * pi),
constexpr_sin(0.5f * 2 * pi),
constexpr_sin(0.6f * 2 * pi),
constexpr_sin(0.7f * 2 * pi),
constexpr_sin(0.8f * 2 * pi),
constexpr_sin(0.9f * 2 * pi),
};
• Self documenting
• Avoids typos
30
Returning a constexpr Array
// Can't use C++14 std::array, since element access is not
// defined as constexpr.
template <typename T, std::size_t N>
class array_result {
private:
constexpr static std::size_t size_ = N;
T data_[N] {}; // T default constructor essential!
public:
constexpr std::size_t size() const { return N; }
32
generate_sine
template <typename T, std::size_t N>
constexpr auto generate_sine() -> array_result<T, N>
{
array_result<T, N> ret; // Init ret
constexpr T pi = pi_num<T>();
for (int i = 0; i < N; i += 1) {
T f = i;
ret[i] = constexpr_sin(f * 2 * pi / N); // Modify ret
}
return ret; // Return ret
}
33
Try generate_sine
constexpr float sine_lookup (std::size_t i)
{
constexpr auto table =
generate_sine<float, 256>();
if (i >= table.size()) {
throw std::out_of_range ("Index too big.");
}
return table[i];
}
void try_sine_lookup ()
{
constexpr float f1 = sine_lookup (100); // Compile time
const float f2 = sine_lookup (100); // May be run time
assert (f1 == f2);
}
34
constexpr_to_upper
template <std::size_t N>
constexpr auto constexpr_to_upper (const char(&in)[N])
-> array_result<char, N>
{
array_result<char, N> out;
35
Test constexpr_to_upper
void test_constexpr_to_upper()
{
static constexpr auto a =
constexpr_to_upper ("Frank Lloyd Wright");
static_assert (a.size() == 19, "Yike!");
static_assert (a[a.size() - 2] == 'T', "Yike!");
std::cout << &a[0] << std::endl;
}
$ ./test_to_upper
FRANK LLOYD WRIGHT
$
.section __TEXT,__const
__ZZ23test_constexpr_to_uppervE1a:
.asciz "FRANK LLOYD WRIGHT"
36
So far we have…
37
constexpr_str_less
constexpr bool constexpr_str_less (
const char* lhs, const char* rhs)
{
std::size_t i = 0;
do
{
if (lhs[i] != rhs[i])
{
return lhs[i] < rhs[i];
}
} while (lhs[i++] != '\0');
return false;
}
38
constexpr_is_alphabetical
constexpr bool constexpr_is_alphabetical (
const char* const* b, const char* const* e)
{
for (const char* const* i = b + 1; i < e; ++i)
{
if (constexpr_str_less(*i, *(i - 1)))
{
return false;
}
}
return true;
}
39
Test constexpr_is_alphabetical
void test_is_alphabetical()
{
constexpr std::size_t count = 4;
constexpr const char* architects[count] =
{
"Daedalus",
"Michelangelo",
"Pei, I. M.",
"Wright, Frank Lloyd"
};
static_assert (
constexpr_is_alphabetical(
&architects[0], &architects[count]),
"architects not alphabetical!");
}
40
Let the Compiler Do the Sort
// Can't default cmp to std::less; it's not constexpr.
template <typename Itr, typename Cmp>
constexpr void constexpr_insertion_sort (Itr b, Itr e, Cmp cmp)
{
static_assert (std::is_same <bool,
decltype (cmp (*b, *e))>::value, "cmp not valid.");
using T = typename std::iterator_traits<Itr>::value_type;
if (b == e) return;
for (Itr i = b + 1; i != e; ++i) {
Itr j = i;
const T temp = i[0];
for (j = i;
(j != b) && cmp(temp, j[-1]); --j) {
j[0] = j[-1];
}
j[0] = temp;
}
}
41
Hold It!
A constexpr function that returns void?
• Illegal in C++11
• Useful in C++14 if…
• Not the outermost constexpr function
42
C++14 constexpr Model
• C++ interpreter
• Each constexpr result has its entire
context
function_b
Params
function_d
function_a
Value
function_c
// Sort
constexpr_insertion_sort (
out.data(), out.size(), constexpr_str_less);
return out;
}
44
Test constexpr_str_sort
void test_str_sort()
{
constexpr const char* architects[] =
{ "Pei", "Daedalus", "Wright", "Michelangelo" };
$ ./test_str_sort
0 : Daedalus
1 : Michelangelo
2 : Pei
3 : Wright
45
Validate or Sort?
• Validation is O(n)
46
Compile-Time Associative Containers
47
Add More Iterators to array_result<>
template <typename T, std::size_t N>
class array_result {
// ...
using const_iterator = const T*;
constexpr const_iterator
begin() const { return &data_[0]; }
constexpr const_iterator
cbegin() const { return &data_[0]; }
constexpr const_iterator
end() const { return &data_[N]; }
constexpr const_iterator
cend() const { return &data_[N]; }
};
48
std::equal_range() With array_result<>
void test_str_sort_find()
{
// Construct sorted array "a" at compile time.
constexpr const char* architects[] =
{ "Pei", "Daedalus", "Wright", "Michelangelo" };
constexpr auto a = constexpr_str_sort (architects);
static_assert (a.size() == 4, "Yike!");
49
constexpr Unordered Containers?
50
hash_and_str
// Can't use std::pair because the assignment
// operator is not constexpr.
struct hash_and_str
{
std::uint32_t hash;
const char* str;
51
Fill In the Hash Container
using hash_fn_t = std::uint32_t (*)(const char*);
52
Sort and Verify the Hash
// ...
// Sort. Can't use a lambda. No lambdas in constexpr.
constexpr_insertion_sort (
out.begin(), out.end(), hash_and_str::less);
53
Perfect Minimal Hash Function
// Minimal perfect hash found manually.
constexpr std::uint32_t architects_hash (const char* in)
{
std::uint32_t h = 2166136261;
for (std::size_t i = 0; in[i] != '\0'; ++i)
{
h = (h * 16777619) ^ in[i];
}
h = h & 0x03;
return h;
}
54
Testing constexpr_str_hash
void test_str_hash()
{
constexpr const char* architects[] =
{ "Pei", "Daedalus", "Wright", "Michelangelo" };
constexpr auto a =
constexpr_str_hash (architects, architects_hash);
57
Topics
• Compile-time parsing
• Compile-time containers
• Compile-time floating point
• Future and Summary
58
Compile-Time Floating Point
59
Motivation
60
Use Case: Biquad Coefficients
• Biquad is a common DSP filter
• Two parts:
1. Coefficients
• Determine filter response
• Typically hard to compute
• Constant for a given response
2. Recursive computation
• Provides filter output
• Typically fast to compute
• Executes once per sample
61
Buckle Up!
62
What We’re Computing…
enum class BiquadType : unsigned char {
lowpass = 0,
highpass,
bandpass,
notch,
peak,
lowshelf,
highshelf
};
template <typename T>
struct Coeffs {
T a0 = 0;
T a1 = 0;
T a2 = 0;
T b1 = 0;
T b2 = 0;
};
63
BiquadCoeffs
template <typename T = double> // T <- storage type
class BiquadCoeffs
{
public:
template <typename U>
constexpr BiquadCoeffs(
BiquadType type, U fc, U q, U peakGainDB)
: type_(type)
, coeffs_(computeCoeffs<T>(type, fc, q, peakGainDB))
{ }
private:
template <typename U> friend class Biquad;
constexpr T process(T in, T& z1, T& z2) const;
private:
BiquadType type_;
Coeffs<T> coeffs_;
};
64
computeCoeffs
template <typename T, typename U> // T <- storage type
constexpr Coeffs<T> computeCoeffs (
BiquadType type, U fc, U q, U peakGainDB)
{
using W = typename std::common_type<T, U>::type;
if ((fc < 0) || (fc >= 0.5f )) {
throw std::domain_error(
"fc must be less than the Nyquist frequency.");
}
Coeffs<T> coeffs;
W norm = 0;
const W v = constexpr_pow<W>(
10, constexpr_abs<W>(peakGainDB) / 20);
const W k = constexpr_tan<W>(pi_num<W>() * fc);
switch (type) {
...
65
Coefficients by Type
case BiquadType::lowpass:
norm = 1 / (1 + k / q + k * k);
coeffs.a0 = static_cast<T>(k * k * norm);
coeffs.a1 = static_cast<T>(2 * coeffs.a0);
coeffs.a2 = coeffs.a0;
coeffs.b1 = static_cast<T>(2 * (k * k - 1) * norm);
coeffs.b2 = static_cast<T>((1 - k / q + k * k) * norm);
break;
case BiquadType::highpass:
norm = 1 / (1 + k / q + k * k);
coeffs.a0 = static_cast<T>(1 * norm);
coeffs.a1 = static_cast<T>(-2 * coeffs.a0);
coeffs.a2 = coeffs.a0;
coeffs.b1 = static_cast<T>(2 * (k * k - 1) * norm);
coeffs.b2 = static_cast<T>((1 - k / q + k * k) * norm);
break;
...
66
Functions Created / Used
• T pi_num<T> ()
• T constexpr_abs<T> (U x)
• T constexpr_pow<T> (U base, V exp)
• T eulers_num<T> ()
• T constexpr_log<T> (U x)
• T constexpr_exp<T> (U x)
• pair<T> constexpr_modf<T> (U x)
• T constexpr_tan<T> (U x)
• T constexpr_sin<T> (U x)
• T constexpr_cos<T> (U x)
67
Using the Coefficients
template <typename T>
constexpr T
BiquadCoeffs<T>::process(T in, T& z1, T& z2) const
{
T out = in * coeffs_.a0 + z1;
z1 = in * coeffs_.a1 + z2 - coeffs_.b1 * out;
z2 = in * coeffs_.a2 - coeffs_.b2 * out;
return out;
}
private:
BiquadCoeffs<T> coeffs_;
T z1_, z2_; // Hold changing recursion values
};
69
Using Biquad
void test_biquad()
{
constexpr BiquadCoeffs<float> coeffs_l
(BiquadType::lowpass, 0.05, 0.707, 0.0);
Biquad<float> lowpass (coeffs_l);
constexpr std::size_t ramp_size = 10;
constexpr float ramp[ramp_size] {
0.0f,0.2f,0.4f,0.6f,0.8f,1.0f,1.2f,1.4f,1.6f,1.8f
};
constexpr int sample_count = 101;
float samples[sample_count];
std::size_t i = 0;
for (int j = 0; j < sample_count; ++j) {
if (i >= ramp_size) {
i = 0;
}
samples[j] = lowpass.process(ramp[i++]);
}
} 70
constexpr BiquadCoeffs Benefits
Declare constexpr BiquadCoeffs
Assembly file 144 lines
71
constexpr Float Timing
constexpr_pow() at compile-time
vs
std::pow()
vs
constexpr_pow() at runtime
72
Timing Code 1
constexpr int repetitions = 10’000'000;
void compile_time()
{
using namespace std::chrono;
constexpr double two = 2.0;
volatile static double sink;
const auto start_t = high_resolution_clock::now();
for (int i = 0; i < repetitions; ++i) {
constexpr double sqrt2 = constexpr_pow (two, 0.5);
sink = sqrt2;
}
const auto end_t = high_resolution_clock::now();
auto elapsed = end_t - start_t;
std::cout << "constexpr_pow at compile time: "
<< duration_cast<nanoseconds>(elapsed).count()
<< " ns" << std::endl;
}
73
Timing Code 2
constexpr int repetitions = 10'000'000;
void run_time()
{
using namespace std::chrono;
volatile double two = 2.0;
volatile static double sink;
const auto start_t = high_resolution_clock::now();
for (int i = 0; i < repetitions; ++i) {
double sqrt2 = std::pow (two, 0.5);
sink = sqrt2;
}
const auto end_t = high_resolution_clock::now();
auto elapsed = end_t - start_t;
std::cout << “std::pow: "
<< duration_cast<nanoseconds>(elapsed).count()
<< " ns" << std::endl;
}
74
Timing Code 3
constexpr int repetitions = 10’000'000;
void constexpr_run_time()
{
using namespace std::chrono;
volatile double two = 2.0;
volatile static double sink;
const auto start_t = high_resolution_clock::now();
for (int i = 0; i < repetitions; ++i) {
double sqrt2 = constexpr_pow (two, 0.5);
sink = sqrt2;
}
const auto end_t = high_resolution_clock::now();
auto elapsed = end_t - start_t;
std::cout << "constexpr_pow at runtime: "
<< duration_cast<nanoseconds>(elapsed).count()
<< " ns" << std::endl;
}
75
Timing Results: Intel i5 2.4 GHz
clang++ -std=c++1y -O3
Normalized…
76
Why the Time Differences?
77
Numeric Errors at Compile Time
void sqrt_neg2_compile_time ()
{
constexpr double neg_sqrt2 = constexpr_pow (-2.0, 0.5);
static_assert (neg_sqrt2 != 0, "Yipe!");
}
78
Numeric Errors at Runtime?
void sqrt_neg2_run_time ()
{
const double neg_sqrt2 = std::pow (-2.0, 0.5);
assert (!std::isnan (neg_sqrt2));
}
79
Floating Point Compiler Magic?
Could the compiler provide a
constexpr value at compile time and
an intrinsic at runtime?
80
Math With Non-constexpr Interfaces
sin(x) cosh(x) floor(x)
cos(x) tanh(x) fabs(x)
tan(x) exp(x) ldexp(x,n)
asin(x) log(x) frexp(x,*e)
acos(x) log10(x) mod(f,*ip)
atan(x) pow(x,y) fmod(x,y)
atan2(y,x) sqrt(x)
sinh(x) ceil(x)
81
No Peeking at Float Internals!
void float_bits_ptr ()
{
static constexpr float f1 {1.0f};
constexpr const unsigned char* raw =
reinterpret_cast<const unsigned char*>(&f1);
}
82
No Peeking at Float Internals!
void float_bits_union ()
{
union f_to_i {
float f32;
unsigned char raw[sizeof (float)];
constexpr f_to_i (float f) : f32(f) { }
};
constexpr f_to_i u (1.0f);
static_assert (u.raw[0] != 0, "Yipe!");
}
• Compile-time parsing
• Compile-time containers
• Compile-time floating point
• Future and Summary
85
Future and Summary
86
Future
87
Summary
Good for:
• Errors caught at compile-time
• Improved code readability
• Reduce executable footprint
• Avoid global init order issues
• Avoid initialization thread races
Accidental use of constexpr code at runtime
can slow execution
• Consider unresolved extern hack
C++ at compile time?
Absolutely!
88
Thanks and Acknowledgements
• Gabriel Dos Reis: constexpr C++11
• Richard Smith: constexpr C++14
• Scott Meyers: great references
• Howard Hinnant: unresolved external
• Scott Determan: slide review
• cppreference.com: constexpr_txt
http://en.cppreference.com/w/cpp/language/constexpr
89
Questions?