Home > other >  destructor is called twice for variant-based implementation
destructor is called twice for variant-based implementation

Time:09-16

I have a variadic variant_callable class object that I want to use for a runtime polymorphism. Inside it uses a visitor pattern with std::variant.
However, I came by a rather strange behavior, that is object's destructor is called twice!.

#include <utility>
#include <variant>
#include <tuple>

namespace detail
{
    template<typename... Impl>
    class variadic_callable
    {
    public:
        template<typename T>
        constexpr explicit variadic_callable(T &&t)   //
          : varImpl_(std::forward<T>(t))
        {}

        variadic_callable(const variadic_callable &) = delete;
        variadic_callable(variadic_callable &&) = delete;

        template<typename... Args>
        constexpr decltype(auto) operator()(Args &&...args) const
        {
            return std::visit(
                [argsTuple = std::forward_as_tuple(args...)](const auto &visitor) {
                    return std::apply(
                        [&visitor](auto &&...args) {
                            return visitor(std::forward<decltype(args)>(args)...);
                        },
                        argsTuple);
                },
                varImpl_);
        }

    private:
        std::variant<Impl...> varImpl_;
    };
}   // namespace detail

#include <string>
#include <iostream>

int main(int, char **)
{
    struct callable
    {
        std::string str = "Long enough string to be allocated. Oceanic"; 

        callable()
        {
            std::cout << "callable()" << std::endl;
        }

        void operator()(int i) const
        {
            std::cout << str << " " << i << '\n';
        }

        ~callable()
        {
            std::cout << "~callable()" << std::endl;
        }
    };

    {
        std::cout << "expcected:\n";
        const auto &c = callable();
        c(815);
        std::cout << "finished\n";
    }
    std::cout << '\n';

    {
        std::cout << "actual\n";
        const auto &w = detail::variadic_callable<callable>{callable()};
        w(815);
        std::cout << "finished\n";
    }

}

The output:

Program returned: 0
expcected:
callable()
Long enough string to be allocated. Oceanic 815
finished
~callable()

actual
callable()
~callable()
Long enough string to be allocated. Oceanic 815
finished
~callable()

https://godbolt.org/z/d849EaqbE

I guess an UB is in-place, but I can't spot it.
What I find the most peculiar is the fact that in the "actual" case std::string resources are not destroyed after the first destructor invocation!

CodePudding user response:

variadic_callable's constructor is being passed an object of type callable. This is a temporary object that cannot be the same object as the one stored in the std::variant (no matter how it is passed).

The callable inside the std::variant must therefore be move-constructed from the passed temporary object. Both of these objects need to be eventually destroyed, requiring two calls to callable's destructor.

To prevent this you need to pass the arguments from which callable is supposed to be constructed to variadic_callable's constructor instead (here an empty list) and then pass these on to std::variants in-place constructor, i.e.

 template<typename T, typename... Args>
 constexpr explicit variadic_callable(std::in_place_type_t<T> t, Args&&... args)   //
   : varImpl_(t, std::forward<Args>(args)...)
 {}

called as

detail::variadic_callable<callable>{std::in_place_type<callable>};

Here I copied std::variant's constructor design for the in-place overload.

  • Related