Constants and Strings

← Back to the blog

In general, there are 3 types of constants.

  1. Variables
  2. Macros
  3. 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.

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.

    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.