Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

Is there a way to reset a std::variant from a known alternative?

Tags:

c++

c++17

variant

I'm in the process of updating a codebase that is currently using a custom equivalent of std::variant to C++17 .

In certain parts of the code, the variant is being reset from a known alternative, so the class provides a method that asserts that index() is at a current value, but still directly invokes the proper destructor unconditionally.

This is used in some tight inner loops, and has (measured) non-trivial performance impact. That's because it allows the compiler to eliminate the entire destruction when the alternative in question is a trivially destructible type.

At face value, it seems to me that I can't achieve this with the current std::variant<> implementation in the STL, but I'm hoping that I'm wrong.

Is there a way to accomplish this that I'm not seeing, or am I out of luck?

Edit: as requested, here's a usage example (using @T.C's example as basis):

struct S {
    ~S();
};

using var = MyVariant<S, int, double>;

void change_int_to_double(var& v){
  v.reset_from<1>(0.0);
}

change_int_to_double compiles to effectively:

@change_int_to_double(MyVariant<S, int, double>&)
  mov qword ptr [rdi], 0       // Sets the storage to double(0.0)
  mov dword ptr [rdi + 8], 2   // Sets the index to 2

Edit #2

Thanks to various insight from @T.C., I've landed on this monstrosity. It "works" even though it does violate the standard by skipping a few destructors. However, every skipped destructor is checked at compile-time to be trivial so...:

see on godbolt: https://godbolt.org/g/2LK2fa

// Let's make sure our std::variant implementation does nothing funky internally.
static_assert(std::is_trivially_destructible<std::variant<char, int>>::value, 
          "change_from_I won't be valid");

template<size_t I, typename arg_t, typename... VAR_ARGS>
void change_from_I(std::variant<VAR_ARGS...>& v, arg_t&& new_val) {
    assert(I == v.index());

    // Optimize away the std::get<> runtime check if possible.
    #if defined(__GNUC__) 
      if(v.index() != I) __builtin_unreachable();
    #else
      if(v.index() != I) std::terminate();
    #endif


    // Smart compilers handle this fine without this check, but MSVC can 
    // use the help.
    using current_t = std::variant_alternative_t<I, std::variant<VAR_ARGS...>>;
    if(!std::is_trivially_destructible<current_t>::value) {
        std::get<I>(v).~current_t();
    }
    new (&v) var(std::forward<arg_t>(new_val));
}
like image 700
Frank Avatar asked Sep 11 '17 03:09

Frank


People also ask

Does std :: variant allocate memory?

No, very explicitly. From [variant. variant]: Any instance of variant at any given time either holds a value of one of its alternative types, or it holds no value.

Where can I use std variant?

union s save memory because they allow a single piece of memory to be used for different types of objects at different times. Consequently, they can be used to save memory when we have several objects that are never used at the same time. std::variant uses the memory similar to the union .

What is Monostate C++?

A monostate is a "conceptual singleton" - all data members of a monostate are static, so all instances of the monostate use the same (static) data. Applications using a monostate can create any number of instances that they desire, as each instance uses the same data.


Video Answer


1 Answers

#include <variant>
struct S {
    ~S();
};
using var = std::variant<S, int, double>;

void change_int_to_double(var& v){
    if(v.index() != 1) __builtin_unreachable();
    v = 0.0;
}

GCC compiles the function down to:

change_int_to_double(std::variant<S, int, double>&):
  mov QWORD PTR [rdi], 0x000000000
  mov BYTE PTR [rdi+8], 2
  ret

which is optimal. Clang's codegen, OTOH, leaves much to be desired, although it isn't too bad if you use std::terminate() (the equivalent of an assertion) rather than __builtin_unreachable():

change_int_to_double(std::__1::variant<S, int, double>&): # @change_int_to_double(std::__1::variant<S, int, double>&)
  cmp dword ptr [rdi + 8], 1
  jne .LBB0_2
  mov qword ptr [rdi], 0
  mov dword ptr [rdi + 8], 2
  ret
.LBB0_2:
  push rax
  call std::terminate()

MSVC...let's not talk about MSVC.

like image 173
T.C. Avatar answered Sep 19 '22 08:09

T.C.