In general, there are 3 types of constants.
- Variables
- Macros
- Literals
Variables
Const is a type qualifier (the same as volatile), meaning that it alters the way an existing type is treated by the compiler.
const int a {2};
Macros
Literals
Literals are values inserted directly into code, like true, 42, etc. Which is referred to as literal constants.
Note that literals have a default type.
- 5 - Integer
- 4.2 - Double
- '' - Character
- "foobar" - const char[6]
- ...
const int a {2};
Here the 2 is a literal integer (It evaluates to itself, and is unchangable).
Like we have seen for other literals, there exists in the language a tool called literal suffixes and prefixes:
Literal Suffixes
Anything which changes the default value of a literal when suffixed:
42U // Unsigned integer 42.0L // Long "foobar"s // String "foobar"sv // String-view
Literal Prefixes
Which is anything which is prepended to a literal, and changes it's type, such as:
012 // - Octal prefix 0xDEADBEEF // Hexadecimal prefix 0b010101 // Binary prefix
Compile time evaluation
Why are constants useful. Because they help keep our code behaving as expected, as well as communicating our intentions. But there are also more quantifiable reasons. And that is that it helps the compiler optimize your code. For instance
Compile time folding
int a {3+4}; // = int a {7};
Constant propagation
That is inserting the value of an expression at compile time.
int a {3+4}; // = int a {7}; std::cout << a << '\n'; // a = 7 at compile time
Dead Code Elimination
Removing code that is never used. For instance
int x {7}; std::cout << 7 << '\n'; // No need to keep the x
Where the compiler can safely remove the allocation of the x variable, and hence save us a full allocation when this code is executed.
Makes it easier to optimize constant variables
As a program knows, it gets harder for the compiler to keep track of all the variables. As such, marking a variable as constant will help the compiler to apply constant propagation as we have seen above.
... const int x {7}; ... std::cout << x << '\n';
Compile time vs Runtime constants
In a way, we can think of two general classes of constants. That is runtime constants, and compile-time constants. Whereas the reason for their naming is pretty self-evident.
- Compile-time
- Literals
- Constant Expressions
- Runtime Constants
- Function parameters
- Variables
const int a {5}; // Compile-time const int b {foo(5)}; // Runtime
Constant Expressions
Constant expressions are a means for the programmer to be explicit about what should be evaluated at compile time. This means that we can require the compiler to handle an expression at compile time, or error if unable to do so. Hence, in any constant expression all parts must be constant expressions in order for it to be a valid constant expression.
Constant expressions are only required to evaluate at compile time in contexts that require a constant expression. Reversely, they are guaranteed to evaluate at compile-time in a constant-expression context.
Constant expressions are thus implicitly const. Hence, Type(constexpr int a) = const int a.
Functions can also be constant expressions. But in order for them to evaluate at compile time, all arguments must be constant expressions.
Standard Strings
The standard library has support for a built-in string type, namely the
std::String. Unlike "foobar"
, like we have seen
before, which is a const char[6]
. A
std::String
is a class type, with methods, and it's own
memory to manage. Keep in mind though, that this means that the
std::String
class is expensive to use, as it will make a
copy of any string it is managing. This means that your regular string
parameters are expensive to use.
Also it means, funnily enough that
std::cout << "foobar"s << '\n';
Is more expensive than
std::cout << "foobar" << '\n';
Because the former requires an allocation, and the latter does not.
But there are ways around this problem of course. It is just important to keep in mind. Which brings us to
Standard String Views
A standard std::string_view
is a class which only thinly
wraps a string, but does no allocation. It is literally just a view into
an already existing string. Thus giving us the ability to shorten it for
example, but never change it.
This means that a std::string_view
can wrap a regular
const char[]
without any further allocation, making it a
good parameter for passing string literals as function
arguments for instance.
It can also simply wrap a regular string class. But then, be careful not to change the underlying string, as the effects will be visible in the string_view. The same goes for having the underlying string go out of scope, and hence create a dangling string_view. Which is definitely not something you want to do.