1

Why is a templated variant return type cause bad_variant_access exception?

Unhandled exception at 0x779508B2 in Project2.exe: Microsoft C++ exception: std::bad_variant_access at memory location 0x00B3FB00.

When in another program, which is bigger, the statement in main got executed without error, but prints out some strange value other than 142. which seemingly is an Undefined behavior.

/////////////////////minified_main.cpp/////////////////
#include <iostream>
#include <variant>


template <typename T>
class THolder {
public:
    THolder() = default;
    THolder(T p) : t_(p) {}
    THolder(const THolder<T>& other) = default;

public:
    T t_;
};

class THolderProducer {
public:
    THolderProducer() = default;
    std::variant<THolder<int>, char*>&& operator()() { return std::move(std::variant<THolder<int>, char*>(THolder<int>{142})); }
};

int main(int argc, char* argv[]) {
    std::cout <<'[' << std::get<0>(THolderProducer()()).t_ << std::endl;
    return 0;
}
Indiana Kernick
  • 5,041
  • 2
  • 20
  • 50
Michaelzh
  • 441
  • 5
  • 14
  • 1
    The compiler [can warn about this](https://gcc.godbolt.org/z/yvrf_C). – chris Oct 24 '19 at 16:37
  • 4
    Your `operator()` returns reference to a temporary. – HolyBlackCat Oct 24 '19 at 16:38
  • changed to ```cpp std::variant, char*>&& operator()() { return std::move(std::variant, char*>(THolder{142})); } ``` Did compile, but printed value is 0, like undefined behavior, – Michaelzh Oct 24 '19 at 16:42
  • 6
    The point is that you can't return a reference to anything local. Just return `std::variant, char*>` instead of a ref. – Lukas-T Oct 24 '19 at 16:44
  • @churill but I did explicitly `std::move`d. – Michaelzh Oct 24 '19 at 16:46
  • a template rarely is the cause for an exception being thrown ;) there are no templates at run-time – 463035818_is_not_an_ai Oct 24 '19 at 16:48
  • Is there something called "Ask Herb Sutter Anything" out there :D ? – Michaelzh Oct 24 '19 at 16:49
  • 6
    @Michaelzh I think `std::move` does not do, what you think it does. – Lukas-T Oct 24 '19 at 16:54
  • 3
    Another instance of the Rvalue Reference Overload Syndrome. Just get rid of all the unneeded rvalue references and random `std::move`s that don't really accomplish anything except create a bunch of temporaries and undefined behavior. – Sam Varshavchik Oct 24 '19 at 16:57
  • I seems like you don't really understand move-semantics, rvalue references or `std::move`. Maybe this will help: https://stackoverflow.com/questions/3106110/what-is-move-semantics – Indiana Kernick Oct 24 '19 at 23:11
  • I only properly understood move-semantics after reading this: https://en.cppreference.com/w/cpp/language/value_category – Indiana Kernick Oct 24 '19 at 23:13
  • @Kerndog73You didn't understand, the question is How come is the undefined behavior, not the Cliché "What is move semantic" – Michaelzh Oct 24 '19 at 23:43
  • 2
    @Michaelzh: it's undefined behavior because you're returning a reference (to a temp) and attempting to use it after the target (the temp) has been destroyed. – Chris Dodd Oct 25 '19 at 00:50

1 Answers1

1

Your program basically boils down to this:

int &&f() {
  return 5;
} 

int main() {
  return f();
}

This is undefined behaviour because you're returning a reference to a temporary. If I compile this with optimizations disabled, the exit code is 5. If I compile with optimizations, the exit code is 176. To me, that is irrefutable proof that the program contains undefined behaviour (or there's a bug in the compiler but in this case I doubt it).

main is accessing a reference to an object whose lifetime has ended. It is undefined what happens when you do that so returning 176 when you expected 5 is perfectly reasonable.


This program can be simplified down further to make it more obvious that this is undefined behaviour.

int *f() {
  int five = 5;
  return &five;
}

int main() {
  return *f();
}

f returns a pointer to an integer allocated on the stack frame of f. When f returns, the stack frame of f is popped from the call stack so five is outside of the call stack. main dereferences a pointer to an object outside of the stack.


While not quite the same, this is pretty similar to accessing elements outside of an array:

int main() {
  int array[1] = {};
  return array[5];
}

In the case of the function that returns a pointer, you're accessing an object that existed at one point but no longer does. In the case of the array, you're accessing an object that never existed in the first place.

Indiana Kernick
  • 5,041
  • 2
  • 20
  • 50