In the previous week, I published the riddle “The Shared View”. Today, we’ll get into the details of this riddle and explain its solution.
General Overview
The code starts with two helper structures: fixed_string
and overloaded
. Let’s take a look over fixed_string
structure:
fixed_string
template<int N> struct fixed_string {
constexpr fixed_string(char const (&s)[N]) {
std::copy_n(s, N, this->elems);
elems[N] = 0;
}
constexpr operator const char*() const { return elems; }
constexpr operator std::string_view() const { return elems; }
char elems[N + 1];
};
This structure allows us to pass a char*
parameter as a non-type template parameter, without the need to hold an external const char
pointer to the string
. It’s currently a limitation in compilers, but it should be resolved in the future.
The way it works is by letting you accept as a non-type template parameter a class object, which can be constructed out of a const char
array. Which means that you can pass a compile time char
array directly as a parameter to the template.
overloaded
template<class... Ts> struct overloaded : Ts... { using Ts::operator()...; };
template<class... Ts> overloaded(Ts...) -> overloaded<Ts...>;
This overloaded
structure simply accepts a variadic template types, and inherits from them. The second line is used as a hit to the compiler for type deduction. The second line won’t be needed in future compilers (at least newer than the ones I tested it on) due to CTAD (class template argument deduction) in C++20 standard. (For a further read about it, visit cppreference.)
In this riddle, this structure will be used as a way to pass to std::visit
function multiple function handlers for different types.
Note: This structure is a complete copy from cppreference – std::visit example.
'A' Structure
template <typename T>
class A {
private:
template<typename T1>
using sptr = std::shared_ptr<T1>;
using p = sptr<T>;
public:
template <typename... Args>
p create(Args&&... args) {
return std::shared_ptr<T>(new T(std::forward<Args>(args)...));
}
template <typename T1>
sptr<T1> get(p inst_p, T1* t1) {
return sptr<T1>(inst_p, t1);
}
};
The A
structure is responsible for generating new std::shared_ptr instances. But here, we have a nuance: the get
function uses a less known std::shared_ptr
constructor named “the aliasing constructor“.
'B' Structure
struct B {
using at = std::variant<int*, double*>;
int a, b;
double c;
at get(const std::string_view& str) {
static const std::unordered_map<std::string_view, at> m {
{ "a", &a },
{ "b", &b },
{ "c", &c }
};
return m.at(str);
}
private:
template <fixed_string Str>
struct get_t {};
static constexpr const char a_str[] = "a";
template <>
struct get_t <a_str> { using type = decltype(a); };
static constexpr const char b_str[] = "b";
template <>
struct get_t <b_str> { using type = decltype(b); };
static constexpr const char c_str[] = "c";
template <>
struct get_t <c_str> { using type = decltype(c); };
public:
template <fixed_string Str>
using get_type = typename get_t<Str>::type;
};
Here, we can see a factory design pattern that is used to get closer to the reflection design pattern.
The function get
creates a static
map that holds a mapping between fields’ names to their corresponding actual address. The function accepts a string_view
parameter, and returns the matched field address on the map (well, it’s a little bit of a lie because it actually returns a std::variant
instance that holds the specific pointer type of the desired data member).
This class holds also some inner (private
) structures that are used to get a field type given its name. The type get_type
is the shortcut for getting the relevant structure inner type. This technique is use a lot within the standard, for example: std::enable_if
& std::enable_if_t
.
'MyAB' Structure
class MyAB {
private:
[[no_unique_address]] A<B> a;
decltype(a.create()) inst;
auto get(auto& t) { return a.get(inst, &t); }
public:
MyAB() : inst(a.create()) {}
auto get() { return inst; }
template <fixed_string VN>
auto get_v() {
std::shared_ptr<B::get_type<VN>> ret;
std::visit(overloaded {
[this, &ret](B::get_type<VN>* v) {
ret = this->get(*v);
},
[](auto*) {}
}, inst->get(VN));
return ret;
}
void reset(decltype(inst)::element_type* p) { inst.reset(p); }
};
This structure is the combination between A
& B
structures. It’s holding (using [[no_unique_address]] attribute) a generator A
class instance, and an actual generated std::shared_ptr
that the generator will create using its create
function (which is std::shared_ptr<B>
).
get_v
function is the most interesting function here. It accepts using non-type template parameter of fixed_string
named VN
. It uses it to get the actual type in B
structure, that matches the VN
name. This type will be used to create a std::shared_ptr
instance of this type. We can see this in the first line inside this function:
std::shared_ptr<B::get_type<VN>> ret;
Now we want to get a pointer to the specific data member of our shared_ptr<B>
instance named inst
. The issue is that this inst
->get
function returns a std::variant
type, so we have to use std::visit
to access it. Moreover, we can’t use a single access function with auto*
param (unless we use if constexpr
inside), so I decided to use the overloaded
structure, and separate the interesting type from the other potential types.
[this, &ret](B::get_type<VN>* v) {
ret = this->get(*v);
}
Inside this lambda expression, we are accessing get
function inside MyAB
instance. This get
function is actually a wrapper for A<B>
generator instance’s get
function, which returns a shared_ptr
instance that constructed using the aliasing constructor, as mentioned before. Now all that is left to do in this function is to return the res
variable.
Question #1 Solution
int main()
{
MyAB b1, b2;
auto t1 = b1.get_v<"c">();
auto t2 = b1.get_v<"a">();
auto t3 = b2.get_v<"a">();
auto t4 = b2.get_v<"c">();
*t1 = 1.2;
*t2 = 3;
*t3 = 4;
*t4 = 5.6;
std::cout << *t1 << *t2 << *t3 << *t4 << "\n";
return 0;
}
Yes, the code will compile and the output is: 5.6445.6
. Something seems a little bit wring around here. We have two separated MyAB
instances, so why do they affect each other?
The answer is hidden within a small keyword that created a huge bug. Inside B
structure, we have a function named get
. This function holds a static
map which points to the specific, current instance, data members. This means that any new instance of this class, will return pointers to the first instance’s data members. The solution is to make this map a non-static
map inside this function, or holding it as a data member that initialized on the constructor, in order to save initialization time in every call to this function.
After solving this issue, the output is 12.345.6
.
Question #2 Solution
int main()
{
MyAB b1, b2;
auto t1 = b1.get_v<"c">();
auto t2 = b1.get_v<"a">();
auto t3 = b2.get_v<"a">();
auto t4 = b2.get_v<"c">();
*t1 = 1.2;
*t2 = 3;
*t3 = 4;
*t4 = 5.6;
std::cout << *t1 << *t2 << *t3 << *t4 << "\n";
b1.reset(nullptr);
t1.reset();
t2.reset();
std::cout << *t3 << *t4;
return 0;
}
In this addition, we are expanding the original bug, and make it more visible. The actual result here is an access violation. The first B
structure instance is completely released (because all the std::shared ptr
that holds a pointer to it got released and now the static
map will return pointers to a memory location that we no longer own.
Question #3 Solution
int main()
{
MyAB b1, b2;
auto t1 = b1.get_v<"c">();
auto t2 = b1.get_v<"a">();
auto t3 = b2.get_v<"a">();
auto t4 = b2.get_v<"c">();
*t1 = 1.2;
*t2 = 3;
*t3 = 4;
*t4 = 5.6;
std::cout << *t1 << *t2 << *t3 << *t4 << "\n";
b1.reset(b2.get().get());
std::cout << *t1 << *t2 << *t3 << *t4 << "\n";
return 0;
}
Now everything might seem ok, but actually we just got a new fresh bug, by using the given API of MyAB
wrongly. b1.reset
will pass a new pointer to the inner std::shared_ptr
to manage. But the pointer that we pass here is already managed by b2 inner std::shared_ptr
instance. So everything will function in a good way, until the second destructor of the std::shared_ptr
that holds this pointer will be called, and then, we’ll get an error saying that we are trying to release a pointer that has already been released.
Summary
std::shared_ptr
aliasing constructor is a dangerous tool, that any usage of it must be coupled with documentation, due to the rare case that it might help with them. The static
keyword is dangerous as well, and should be considered carefully when in use. Hope this riddle refreshed some contents for you, or helped you to get a better understanding of the topics that were being used here. Until next time, don’t be afraid of C++, you’ll get used to it one day.