More constexpr with optional and view types
- What about classes where not all member variables can be initialized at compile-time?
- Use
std::optional
for conditionally used data - Define a "Null Object".
- Lazy initialization - one instance for everyone
- Lazy initialization - multiple instances
- Improvements without applying a complex multithreaded algorithm
- Why …?
- Conclusion
This is a follow-up of these notes for initialising more constants with constexpr
.
Here I am exploring how to initialise at compile time classes where, at least for one member variable, it is not possible to use std::variant
and a corresponding view type.
What about classes where not all member variables can be initialized at compile-time?
The presented approach, of using std::variant
and corresponding view types, does not work well if a member variable cannot be initialized as constexpr
.
Consider, for example, the following class:
class my_class_with_vector{
std::vector<int> v;
public:
const std::vector<int>& data() const{
return v;
}
};
One could use std::variant<std::span<const int>, std::vector<int>>
instead of std::vector<int>
as a member variable for initializing the class as constexpr, but how should the implementation of data()
look like? Since it returns a reference to a std::vector<int>
, one needs to store a std::vector<int>
somewhere.
Another example would be a class with more member variables, but some that are not used in all methods.
struct complex_type {
explicit complex_type(int); // not constexpr, and no corresponding view type
int get_value() const; // not constexpr
};
struct my_class{
std::string str;
complex_type c; // not used in const methods
explicit my_class(std::string str, complex_type c) : str(str), c(c) {}
explicit my_class(std::string str) : my_class(str, complex_type(-1)) {}
// does not use member variable c
int get_data() const {
auto data = std::visit([](std::string_view arg) { return arg; }, str);
return str.empty() ? 1 : str.length();
}
// uses member variable c
int get_data2() const {
return (str.empty() or (c.get_value() == 42)) ? 1 : str.length();
}
};
Even if only one member variable cannot be initialized at compile-time, then the whole instance cannot be made constexpr
!
This is problematic for more complex classes, where some member variables are only conditionally used, and in particular can be avoided when the instance is saved as a constant.
One might argue that if some member variables are conditionally used, then it might make sense to split the class.
This has the same disadvantage of creating a corresponding view class: conversion between types that leads to additional costly copies at runtime, and code duplication.
Note 📝 | my_class might contain a simple int , but if the constructor of my_class calls a function to calculate the value to be stored as a member variable, and if the algorithm is not constexpr , then the issue is similar to having a class like complex_type as a member variable. For simplicity, I’ll continue to use only complex_type in the examples. |
Use std::optional
for conditionally used data
One can use std::optional
for data that might not be used, or for initializing some variables at a later point.
This makes the code more complex, as we need to change the function that uses the optional member variable.
#include <string_view>
#include <string>
#include <variant>
#include <optional>
// external class, cannot be changed
struct complex_type {
explicit complex_type(int); // not constexpr, and no corresponding view type
int get_value() const; // not constexpr
};
struct my_class2 {
std::variant<std::string_view, std::string> str;
std::optional<complex_type> oc;
consteval explicit my_class2(std::string_view str_) : str(str_) {}
consteval explicit my_class2(const char* str_) : my_class2(std::string_view(str_)) {}
explicit my_class2(std::string str, complex_type c) : str(str) , oc(c){}
explicit my_class2(std::string str) : my_class2(str, complex_type(-1)){}
// does not use member variable c
int get_data() const {
auto data = std::visit([](std::string_view arg) { return arg; }, str);
return data.empty() ? 1 : data.length();
}
// uses member variable c
int get_data2() const {
auto data = std::visit([](std::string_view arg) { return arg; }, str);
// NOTE: as user of complex_type, we know that the condition c.get_value() == 42 never holds when complex_type is initialized with -1
return (data.empty() or (c.has_value() and c->get_value() == 42) or ( /* ??? */ )) ? 1 : data.length();
}
// other member functions
};
With std::variant
, the logic was more or less unchanged because std::string
and std::string_view
are both convertible to std::string_view. But for `std::optional
, there is no conversion between the absence of an instance of complex_type
and an instance of complex_type
to a common type, making it necessary to add multiple has_value()
in the logic of the code, and decide what to do when there is no value.
Define a "Null Object".
If the class has a good candidate for a "Null Object", the code can be rewritten as
#include <string_view>
#include <string>
#include <variant>
#include <optional>
// external class, cannot be changed
struct complex_type {
explicit complex_type(int); // not constexpr, and no corresponding view type
int get_value() const; // not constexpr
};
struct optional_complex_type{
std::optional<complex_type> o;
consteval optional_complex_type() noexcept = default;
explicit optional_complex_type(complex_type c):o(std::move(c)){}
int get_value() const {
return o ? -1 : o->get_value();
}
};
struct my_class3 {
std::variant<std::string_view, std::string> str;
optional_complex_type oc;
consteval explicit my_class3(std::string_view str_) : str(str_) {}
consteval explicit my_class3(const char* str_) : my_class3(std::string_view(str_)) {}
explicit my_class3(std::string str, complex_type c) : str(str) , oc(c){}
explicit my_class3(std::string str) : my_class3(str, complex_type(-1)){}
// does not use member variable c
int get_data() const {
auto data = std::visit([](std::string_view arg) { return arg; }, str);
return data.empty() ? 1 : data.length();
}
int get_data2() const {
auto data = std::visit([](std::string_view arg) { return arg; }, str);
return c.get_value() == 42 ? 1 : data.length();
}
// other member functions
};
If it is not possible, you might want to use one of the techniques used for forcing null checks. It would be a pity to introduce bugs in the code because someone forgot to verify if std::optional
is engaged.
Unfortunately, even if there exists a sensible "Null Object", it might be that it cannot be used as an implementation detail.
In the case of my_class_with_vector
, we need to return an instance of std::vector<int>
. Defining other types does not help; we need to store somewhere a std::vector<int>
with the correct data.
Lazy initialization - one instance for everyone
If the instance of a member variable is needed, like in my_class_with_vector
, and it cannot be created as constexpr
, then there is only one way to make the global variable constexpr: initialize the member variable lazily.
In the case of my_class
, it could be that we can share an instance of such a member variable between all instances of my_class_with_complex_type2
. In this case, the C++ language provides a mechanism for lazily creating values out-of-the-box; the singleton pattern. Together with std::optional
, this can be used in the following way.
#include <string_view>
#include <string>
#include <variant>
#include <optional>
// external class, cannot be changed
struct complex_type {
explicit complex_type(int); // not constexpr, and no corresponding view type
int get_value() const; // not constexpr
};
const complex_type& cond_lazy_init(const std::optional<complex_type>& ov) {
if(ov){ return *ov;}
static const auto v = complex_type(-1)
return v;
}
struct my_class4 {
std::variant<std::string_view, std::string> str;
std::optional<complex_type> oc;
consteval explicit my_class4(const char* str_) : my_class4(std::string_view(str_)) {}
consteval explicit my_class4(std::string_view str_) : str(str_) {}
explicit my_class4(std::string str, complex_type c) : str(str) , oc(c){}
explicit my_class4(std::string str) : my_class4(str, complex_type(-1)){}
// does not use member variable c
int get_data() const {
auto data = std::visit([](std::string_view arg) { return arg; }, str);
return data.empty() ? 1 : data.length();
}
int get_data2() const {
auto data = std::visit([](std::string_view arg) { return arg; }, str);
const auto& c = cond_lazy_init(oc);
return c.get_value() == 42 ? 1 : data.length();
}
// other member functions
};
Contrary to the approach used for my_class2
, we never need to verify if std::optional<complex_type>
has been initialized except in the helper function cond_lazy_init
.
get_data2
is mostly unchanged; the function operates on complex_type
directly as in the original my_class
.
std::optional<complex_type>
is always engaged, unless the class has been instantiated in a constexpr
context.
And this approach also works for my_class_with_vector
, as cond_lazy_init
returns a reference to the relevant data.
Lazy initialization - multiple instances
If one instance is not enough for everyone, then using the singleton pattern for a member variable is not possible. One needs to initialize the data "by hand", and also ensure that the initialization is thread-safe.
It is one of those use-cases I’ve never had.
In the following example, complex_type
is either provided by the user or depends on the input string, and is not constructed from a constant value.
#include <string_view>
#include <string>
#include <variant>
#include <optional>
#include <mutex>
// external class, cannot be changed
struct complex_type {
explicit complex_type(int); // not constexpr, and no corresponding view type
int get_value() const; // not constexpr
};
complex_type& cond_lazy_init(std::optional<complex_type>& v, int v) {
static std::mutex mutex;
auto l = std::lock_guard<std::mutex>(mutex);
if(not v) {v = complex_type(sv);}
return *v;
}
struct my_class5 {
std::variant<std::string_view, std::string> str;
mutable std::optional<complex_type> oc;
consteval explicit my_class5(std::string_view str_) : str(str_) {}
consteval explicit my_class5(const char* str_) : my_class5(std::string_view(str_)) {}
explicit my_class5(std::string str, complex_type c) : str(str) , oc(c){}
explicit my_class5(std::string str) : my_class5(str, complex_type(str.size())){}
// does not use member variable c
int get_data() const {
auto data = std::visit([](std::string_view arg) { return arg; }, str);
return data.empty() ? 1 : data.length();
}
int get_data2() const {
auto data = std::visit([](std::string_view arg) { return arg; }, str);
const auto& c = cond_lazy_init(oc, data.size());
return c.get_value() == 42 ? 1 : data.length();
}
// other member functions
};
In the case of my_class5
, one also needed to use mutable
, as we are lazily initialising the member variable, and not relying on a separate global instance to use as a fallback.
Note that the mutex is always locked, even if complex_type
was initialized during construction. It might not be efficient, but at least it works, and the code is as simple as it gets, and hopefully free of race conditions.
Improvements without applying a complex multithreaded algorithm
Since I do not trust myself (yet) to write a more performant algorithm 🗄️ for conditionally initializing the data, what if I avoid doing the lazy initialization at all if the value is initialized on construction, and apply the (inefficient) initialization algorithm only if lazy-initialization is necessary?
It is possible to store a separate (constant) tag to determine if lazy initialization is necessary or not. Also, while I am at it, I decided to wrap the optional, to avoid accessing it by accident bypassing the mutex
#include <string_view>
#include <string>
#include <variant>
#include <optional>
#include <mutex>
// external class, cannot be changed
struct complex_type {
explicit complex_type(int); // not constexpr, and no corresponding view type
int get_value() const; // not constexpr
};
class wrapper{
mutable std::optional<complex_type> v;
bool init_lazy = true; // const, except for wrapper& operator=(const wrapper&), wrapper& operator=(wrapper&&)
public:
consteval explicit wrapper() noexcept = default;
explicit wrapper(complex_type c) : v(std::move(c)), init_lazy(false){}
const complex_type& cond_lazy_init(int i) const {
if(init_lazy){
static std::mutex mutex;
auto l = std::lock_guard<std::mutex>(mutex);
if(not v) {v = complex_type(i);}
}
return *v;
}
};
struct my_class6 {
std::variant<std::string_view, std::string> str;
wrapper w;
consteval explicit my_class6(std::string_view str_) : str(str_) {}
consteval explicit my_class6(const char* str_) : my_class6(std::string_view(str_)) {}
explicit my_class6(std::string str, complex_type c) : str(str) , w(c){}
explicit my_class6(std::string str) : my_class6(str, complex_type(str.size())){}
// does not use member variable c
int get_data() const {
auto data = std::visit([](std::string_view arg) { return arg; }, str);
return data.empty() ? 1 : data.length();
}
int get_data2() const {
auto data = std::visit([](std::string_view arg) { return arg; }, str);
const auto& c = w.cond_lazy_init(data.size());
return c.get_value() == 42 ? 1 : data.length();
}
// other member functions
};
With this setup, there is one possible additional improvement: copy and move constructors.
std::variant<std::string_view, std::string>
can be copied as-is, but for std::optional<complex_type>
, there is a choice if std::optional
is empty.
Either copy the empty optional or eagerly initialize the value.
The main advantage of eagerly initializing the value is that all usages of the copied element will not lock the mutex. I think for the general use case, it might be more beneficial, at least with the current cond_lazy_init
algorithm.
The copy constructor would thus look like:
constexpr my_class6(const my_class6& o): str(o.str) {
if consteval {
// comptime, copy as-is
this->w = o.w;
} else {
// runtime, ensure that w contains a value, so that copies do not need to verify if initialized every time
auto data = std::visit([](std::string_view arg) { return arg; }, str);
this->w = wrapper(o.w.cond_lazy_init(data.size()));
}
}
// call copy op
constexpr my_class6& operator=(const my_class6& o) {
this-> str = o.str;
if consteval {
// comptime, copy as-is
this->w = o.w;
} else {
// runtime, ensure that w contains a value, so that copies do not need to verify if initialized every time
auto data = std::visit([](std::string_view arg) { return arg; }, str);
this->w = wrapper(o.w.cond_lazy_init(data.size()));
}
return *this;
}
constexpr my_class6(my_class6&& o) = default;
constexpr my_class6& operator=(my_class6&& o) = default;
If one is thinking about modifying the move constructor: don’t. The same considerations done here also hold in this case. In particular, the move constructor cannot be used on the global instances.
What about the assignment operator?
It would normally change both init_lazy
and oc
, which is problematic. In fact, it is an issue for all member variables!
So the assumption is that data referenced between multiple threads is not reassigned. After all, I’m not designing a thread-safe my_class
. All mutable operations of the class, if not stated otherwise, will cause a race condition.
Also note that init_lazy
should be true
only for constexpr instances, especially when using a tagged constructor.
Why …?
Why use a mutex at all?
Before adding any lazily initialized data, the class had not mutex.
So why am I adding one?
Globals are generally accessed from multiple threads, since constants do not change "phyisically"[1], they are automatically thread-safe. By adding a mutable
variable, a const
instance is not "physically" immutable any more, but only "logically". Thus I needed to use a mutex to make it thread-safe again.
Non-const
methods are, by default, not thread-safe.
Unless my_class
was already thread-safe for mutable methods, there is no need to add a mutex just for initializing a variable in a thread-safe way.
Another case would be that the lazily initialized data is only used in mutable methods. Also in this case, there would be no need to mark the variable mutable
and add a mutex, unless the mutable methods are supposed to be thread-safe too.
Why not use mutexed_obj?
Because the same mutex is used for protecting an indeterminate number of globals.
If the lazy initialization does not happen often, it is better not to add additional mutex to every instance of my_class
, which would also make the class non-copyable and non-moveable.
Also considering that a mutex for a local instance is unnecessary, it would worsen every usage of my_class
.
Why not use std::call_once
?
Why not use a member variable std::once_flag
and use the function std::call_once
, which should be more efficient than always locking a mutex when the data is initialized lazily?
class wrapper{
mutable std::optional<complex_type> v;
mutable std::once_flag flag;
public:
consteval explicit wrapper()=default;
explicit wrapper(complex_type c) : v(std::move(c)), init_lazy(false){}
const complex_type& cond_lazy_init(int i) const {
std::call_once(flag, [&](){ if(not v) {v = complex_type(i);} });
return *v;
}
};
The code looks reasonable, but the main issue is that std::once_flag
cannot be copied or moved, which makes also my_class
impossible to copy or move.
Why use std::optional
and a bool
flag? It is not space-efficient!
That’s true, on my machine following invariants hold:
static_assert(sizeof(bool) == 1);
static_assert(sizeof(bool) == sizeof(unsigned char));
static_assert(sizeof(std::once_flag) == 4*sizeof(bool));
static_assert(sizeof(std::optional<complex_type>) == sizeof(complex_type) + sizeof(bool));
static_assert(sizeof(wrapper) == sizeof(complex_type) + 2*sizeof(bool));
If instead of using std::optional
I would be using a buffer of type unsigned char
, and a tristate-flags, I could make sizeof(wrapper)
smaller by one sizeof(bool)
.
As using a buffer as storage for other types is error-prone, I do not think that it is normally worth the effort.
Why not use std::variant
instead of std::optional
If one needs to store some constexpr
data separately for initializing complex_type
, then using std::variant
seems like a better alternative.
In the example I rote until now, I’ve used std::optional
, because there was no need to store separately other informations.
But if the values for creating complex_type
needs to be store seomwhere, and it can be done as constexpr
, then with std::variant
it is possible to write something similar to the following:
#include <string_view>
#include <string>
#include <variant>
#include <optional>
#include <mutex>
// external class, cannot be changed
struct complex_type2 {
explicit complex_type2(int, int); // not constexpr, and no corresponding view type
int get_value() const; // not constexpr
};
struct data_for_creating_complex_type2{
int a;
int b;
};
class wrapper{
mutable std::variant<data_for_creating_complex_type2, complex_type2> v;
bool init_lazy = true; // const, except for wrapper& operator=(const wrapper&), wrapper& operator=(wrapper&&)
public:
consteval explicit wrapper(data_for_creating_complex_type2 d) : v(d){}
explicit wrapper(complex_type2 c) : v(std::move(c)), init_lazy(false){}
const complex_type2& cond_lazy_init() const {
if(init_lazy){
static std::mutex mutex;
auto l = std::lock_guard<std::mutex>(mutex);
if (const auto* pv = std::get_if<data_for_creating_complex_type2>(&v)){
v = complex_type2(pv->a, pv->b);
}
return std::get<complex_type2>(v);
}
return std::get<complex_type2>(v);
}
};
struct my_class7 {
std::variant<std::string_view, std::string> str;
wrapper w;
consteval explicit my_class7(std::string_view str_) : str(str_), w(data_for_creating_complex_type2{1, 0}) {}
consteval explicit my_class7(const char* str_) : my_class7(std::string_view(str_)) {}
explicit my_class7(std::string str, complex_type2 c) : str(str) , w(c){}
explicit my_class7(std::string str) : my_class7(str, complex_type2(1,0)){}
// does not use member variable c
int get_data() const {
auto data = std::visit([](std::string_view arg) { return arg; }, str);
return data.empty() ? 1 : data.length();
}
int get_data2() const {
auto data = std::visit([](std::string_view arg) { return arg; }, str);
const auto& c = w.cond_lazy_init();
return c.get_value() == 42 ? 1 : data.length();
}
// other member functions
};
Conclusion
Making a class constexpr
-constructible can lead to simpler code for the user.
There is less need to wrap every constant in a function to avoid the initialization order fiasco and possible exceptions when initialising globals. Another advantage is that it reduces the amount of required resources and leads to smaller binaries. At least where I’ve used these techniques.
Unfortunately, it is not possible to change all classes, for example, standard library types. Even if the constructors are marked constexpr
, it does not mean that they can be used for initializing constexpr
global variables 🗄️. And even if it is possible, it might depend on the compiler and standard library.
For example, the default constructor of std::string
is marked constepxr
, and if it does not allocate, then it can be used for initializing data at compile time.
Unfortunately, this is not possible in MSVC, as even the empty string allocates some memory 🗄️. Thus, if your class has a std::string
, that is always empty when the class is initialized as a constant, the code will compile with the default standard libraries of Clang and GCC, but fail with the one provided by MSVC.
From the moment one needs to lazily initialize some data, and once that happens, wrapping a constant in a function is an easier alternative.
The TL;DR is that not every class should be made constexpr
constructible, but classes that are often used for defining constants should be. In case of lazy initialization of member variables, consider if the effort is worth it.
If you have questions, comments, or found typos, the notes are not clear, or there are some errors; then just contact me.