Search code examples
c++arraysvariant

Lose reference to std::variant object after changing its type


Note that the problem can be reproduced by running the snippet below (I use wandbox with gcc 9.1)

So I have a std::array (size 2 for simplicity) of std::variant of two custom types (Normal and Special) Normal is specified as first type so upon class construction, the array is default-constructed with Normal objects. I change some internal data members of the first element of the array and print it out. Looks fine.

Now I want to set the second element of the array to Special object. I tried doing this both by assigning to a new value and using emplace according to this tutorial (https://www.bfilipek.com/2018/06/variant.html#changing-the-values)

However, when I try to change the internal data members of the second object (now typed Special) it seems like I'm not operating on the object in the original arrays. Print out results show default value of construction (0 in this case) I am new to using std::variant so I don't have a clue why it would be the case. How can I get the actual reference to the recently type-change variant object in my array?

#include <iostream>
#include <memory>
#include <cstring>
#include <array>
#include <variant>

struct Normal {
    struct Header {
        std::array<uint8_t, 2> reserved;
    };
    Normal() : frame{0}, payload{reinterpret_cast<uint8_t*>(frame + sizeof(Header))} {}
    constexpr static auto LENGTH = 10;
    uint8_t frame[LENGTH];
    uint8_t* payload;
};

struct Special {
    struct Header {
        std::array<uint8_t, 3> reserved;
    };
    Special() : frame{0}, payload{reinterpret_cast<uint8_t*>(frame + sizeof(Header))} {}
    constexpr static auto LENGTH = 11;
    uint8_t frame[LENGTH];
    uint8_t* payload;
};

std::array<std::variant<Normal, Special>, 2> handlers;
Normal* normal_handler;
Special* special_handler;

int main() {
    auto& nh = std::get<Normal>(handlers[0]);
    memset(nh.payload, 3, 3);
    normal_handler = &nh;

    handlers[1].emplace<1>(Special{});
    auto& sh = std::get<Special>(handlers[1]);
    memset(sh.payload, 4 ,4);
    // memset(std::get<Special>(handlers[1]).payload, 4, 4);
    special_handler = &sh;

    for (int i = 0; i < 10; i++) {
        // Expect 3 bytes from 3rd bytes = 3
        std::cout << (int) normal_handler->frame[i] << " ";
    }

    std::cout << std::endl;

    for (int i = 0; i < 11; i++) {
        // Expect 4 bytes from 4th bytes = 4
        std::cout << (int) special_handler->frame[i] << " ";
        // std::cout << (int) std::get<Special>(handlers[1]).frame[i] << " ";
    }

}

Solution

  • Your issue isn't related to std::variant, the following code shows the same behaviour:

    #include <iostream>
    #include <memory>
    #include <cstring>
    
    struct Special {
        struct Header {
            std::array<uint8_t, 3> reserved;
        };
        Special() : frame{0}, payload{reinterpret_cast<uint8_t*>(frame + sizeof(Header))} {}
        constexpr static auto LENGTH = 11;
        uint8_t frame[LENGTH];
        uint8_t* payload;
    };
    
    int main() {
    
        Special s1;
        s1 = Special{};
        memset(s1.payload, 4 ,4);
    
        for (int i = 0; i < 11; i++) {
            // Expect 4 bytes from 4th bytes = 4
            std::cout << (int) s1.frame[i] << " ";
        }
    }
    

    This line:

        s1 = Special{};
    

    Creates a temporary Special object and then assigns it to s1. The default copy and move constructors will set s1.payload to the value of payload in the temporary. Therefore s1.payload is a dangling pointer to frame in the temporary object and the rest of your code therefore has undefined behaviour.

    The simplest fix is to change the payload member into a function:

    #include <iostream>
    #include <memory>
    #include <cstring>
    
    struct Special {
        struct Header {
            std::array<uint8_t, 3> reserved;
        };
        Special() : frame{0} {}
        constexpr static auto LENGTH = 11;
        uint8_t frame[LENGTH];
        uint8_t* payload() { return &frame[sizeof(Header)]; }
    };
    
    int main() {
    
        Special s1;
        s1 = Special{};
        memset(s1.payload(), 4 ,4);
    
        for (int i = 0; i < 11; i++) {
            // Expect 4 bytes from 4th bytes = 4
            std::cout << (int) s1.frame[i] << " ";
        }
    
    }