Click here to Skip to main content
15,889,909 members
Articles / Programming Languages / C++

The Solution to the Shared View Riddle

Rate me:
Please Sign up or sign in to vote.
0.00/5 (No votes)
22 Apr 2023CPOL5 min read 3.3K  
Involves usage example of shared_ptr aliasing constructor, design patterns reflection and factory, metaprogramming, string_view and variant/visit.

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

C++
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

C++
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

C++
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

C++
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

C++
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:

C++
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.

C++
[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

C++
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

C++
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";
    
    // Addition
    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

C++
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";

    // Addition
    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. 😉

License

This article, along with any associated source code and files, is licensed under The Code Project Open License (CPOL)


Written By
Software Developer (Senior)
Israel Israel
Senior C++ developer.
C++ Senioreas blog writer.
Passionate about investigating the boundaries and exploring the advanced capabilities of C++.

Comments and Discussions

 
-- There are no messages in this forum --