Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

Avoid extra copy when creating a tuple from return values in C++

I'm trying to create an object that stores a tuple of objects created from a static function but when the tuple is created, the objects get copied into the tuple instead of being placed directly into it, calling the destructor of each object in the tuple twice, a behavior I would like to avoid. Is there a way to fix this without having to create custom move/copy constructors for each plugin class?

The code is as follows:

#include <tuple>
#include <iostream>

namespace details
{
    template<typename PluginT, typename ContainerT, typename TupleT, size_t... Is>
    static PluginT construct_plugin(ContainerT& container, TupleT&& tuple, std::index_sequence<Is...>) 
    {
        return PluginT(container, std::get<Is>(std::forward<TupleT>(tuple))...);
    }

    template<typename PluginT, typename ContainerT, typename TupleT>
    static PluginT construct_plugin(ContainerT& container, TupleT&& tuple) 
    {
        return construct_plugin<PluginT>(container, std::forward<TupleT>(tuple), std::make_index_sequence<std::tuple_size<std::decay_t<TupleT>>::value>{});
    }
}

struct simple_plugin
{
    template<typename ContainerT>
    simple_plugin(ContainerT& container) {}

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

struct plugin_with_params
{
    template<typename ContainerT>
    plugin_with_params(ContainerT& container, int argc, char* argv[]) {}

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

template<typename... PluginTs>
struct plugin_container
{
    std::tuple<PluginTs...> plugins;

    template<typename... TupleTs>
    plugin_container(TupleTs&&... tuples) :
        plugins(details::construct_plugin<PluginTs>(*this, std::forward<TupleTs>(tuples))...) {}
};


int main(int argc, char* argv[])
{
    plugin_container<simple_plugin, plugin_with_params> container(std::make_tuple(), std::make_tuple(argc, argv));
    return 0;
}

And here is the behaviour in action: https://godbolt.org/z/bqjv5r88x

like image 704
Alexandre Deus Avatar asked Sep 03 '25 16:09

Alexandre Deus


2 Answers

struct nocopy{
  nocopy(){}
  nocopy(nocopy const&)=delete;
  nocopy(nocopy&&)=delete;
};
struct maker{
  operator nocopy()const{return {};}
};
std::tuple<nocopy,nocopy> t( maker{}, maker{} );

this bypasses the copy constructor in c++17 or above.

A similar technique can be used to get arbitrary functions that return an object to work.

The trick is that there isn't a constructor that accepts the maker object, then C++ falls back on invoking the converting operator on the maker, whose return value can be elided directly into the object.

Prior to 17, this may bypass the copy ctor at runtime but won't compile unless copy (or move) exists (even if the copy/move ctor has side effects). This is because 17 added guaranteed elision; before that it was optional.

like image 129
Yakk - Adam Nevraumont Avatar answered Sep 05 '25 07:09

Yakk - Adam Nevraumont


With the static function it can't work, because elision through the constructor parameter of the std::tuple is impossible.

Unfortunately std::tuple lacks a std::piecewise_construct constructor like std::pair has, which could be used to have the action of your construct_plugin be performed in-place in the constructor of the tuple/pair.

You can write your own tuple implementation supporting that, or you can add constructor overloads to your plugins that support construction from a tuple in the way that currently your static helper function does, or you can use the conversion function trick described in the other answer.


The reason that a copy is done rather than a move is only because you declared the destructors and copy constructors. Had you declared none then moves would have been used instead. Defining copy/move constructors can't help you avoid that they are called. The elision is impossible regardless of how the plugin classes are defined.


Also, you probably want std::forward_as_tuple instead of std::make_tuple, since the latter also makes a copy of the individual object before passing on to the plugin constructor, which I don't think is what you want (not that it would matter for the built-in types you are passing around).

like image 27
user17732522 Avatar answered Sep 05 '25 07:09

user17732522