Values known during compilation are privileged. They may be placed in read-only memory, for example, and, especially for developers of embedded systems, this can be a feature of considerable importance. Of broader applicability is that integral values that are constant and known during compilation can be used in contexts where C++ requires an integral constant expression . Such contexts include specification of array sizes, integral template arguments (including lengths of std::array
objects), enumerator values, alignment specifiers, and more. If you want to use a variable for these kinds of things, you certainly want to declare it constexpr
, because then compilers will ensure that it has a compile-time value:
int sz; // non-constexpr variable
…
constexpr auto arraySize1 = sz; // error! sz's value not
// known at compilation
std::array data1; // error! same problem
constexpr auto arraySize2 = 10; // fine, 10 is a
// compile-time constant
std::array data2; // fine, arraySize2
// is constexpr
Note that const
doesn't offer the same guarantee as constexpr
, because const
objects need not be initialized with values known during compilation:
int sz; // as before
…
const auto arraySize = sz; // fine, arraySize is
// const copy of sz
std::array data; // error! arraySize's value
// not known at compilation
Simply put, all constexpr
objects are const
, but not all const
objects are constexpr
. If you want compilers to guarantee that a variable has a value that can be used in contexts requiring compile-time constants, the tool to reach for is constexpr
, not const
.
Usage scenarios for constexpr
objects become more interesting when constexpr
functions are involved. Such functions produce compile-time constants when they are called with compile-time constants . If they're called with values not known until runtime, they produce runtime values. This may sound as if you don't know what they'll do, but that's the wrong way to think about it. The right way to view it is this:
• constexpr
functions can be used in contexts that demand compile-time constants. If the values of the arguments you pass to a constexpr
function in such a context are known during compilation, the result will be computed during compilation. If any of the arguments' values is not known during compilation, your code will be rejected.
• When a constexpr
function is called with one or more values that are not known during compilation, it acts like a normal function, computing its result at runtime. This means you don't need two functions to perform the same operation, one for compile-time constants and one for all other values. The constexpr
function does it all.
Suppose we need a data structure to hold the results of an experiment that can be run in a variety of ways. For example, the lighting level can be high, low, or off during the course of the experiment, as can the fan speed and the temperature, etc. If there are n
environmental conditions relevant to the experiment, each of which has three possible states, the number of combinations is 3 ⁿ . Storing experimental results for all combinations of conditions thus requires a data structure with enough room for 3 ⁿ values. Assuming each result is an int
and that n is known (or can be computed) during compilation, a std::array
could be a reasonable data structure choice. But we'd need a way to compute 3 ⁿ during compilation. The C++ Standard Library provides std::pow
, which is the mathematical functionality we need, but, for our purposes, there are two problems with it. First, std::pow
works on floating-point types, and we need an integral result. Second, std::pow
isn't constexpr
(i.e., isn't guaranteed to return a compile-time result when called with compile-time values), so we can't use it to specify a std::array
's size.
Fortunately, we can write the pow
we need. I'll show how to do that in a moment, but first let's look at how it could be declared and used:
constexpr // pow's a constexpr func
int pow(int base, int exp) noexcept // that never throws
{
… // impl is below
}
constexpr auto numConds = 5; // # of conditions
std::arraypow(3, numConds)> results; // results has
// 3^numConds
// elements
Recall that the constexpr
in front of pow
doesn't say that pow
returns a const
value, it says that if base
and exp
are compile-time constants, pow
's result may be used as a compile-time constant. If base
and/or exp
are not compile-time constants, pow
's result will be computed at runtime. That means that pow
can not only be called to do things like compile-time-compute the size of a std::array
, it can also be called in runtime contexts such as this:
auto base = readFromDB("base"); // get these values
auto exp = readFromDB("exponent"); // at runtime
auto baseToExp = pow(base, exp); // call pow function
// at runtime
Because constexpr
functions must be able to return compile-time results when called with compile-time values, restrictions are imposed on their implementations. The restrictions differ between C++11 and C++14.
In C++11, constexpr
functions may contain no more than a single executable statement: a return
. That sounds more limiting than it is, because two tricks can be used to extend the expressiveness of constexpr
functions beyond what you might think. First, the conditional “ ?:
” operator can be used in place of if-else
statements, and second, recursion can be used instead of loops. pow
can therefore be implemented like this:
constexpr int pow(int base, int exp) noexcept {
return (exp == 0 ? 1 : base * pow(base, exp - 1));
}
This works, but it's hard to imagine that anybody except a hard-core functional programmer would consider it pretty. In C++14, the restrictions on constexpr
functions are substantially looser, so the following implementation becomes possible:
constexpr int pow(int base, int exp) noexcept // C++14
{
auto result = 1;
for (int i = 0; i < exp; ++i) result *= base;
return result;
}
constexpr
functions are limited to taking and returning literal types , which essentially means types that can have values determined during compilation. In C++11, all built-in types except void
qualify, but user-defined types may be literal, too, because constructors and other member functions may be constexpr
:
class Point {
public:
constexprPoint(double xVal = 0, double yVal = 0) noexcept
: x(xVal), y(yVal)
{}
constexprdouble xValue() const noexcept { return x; }
Читать дальше