是否可以避免在C++中调用复制构造函数

Is it possible to avoid copy-constructor call in C++

提问人:TheMemeMachine 提问时间:12/2/2021 最后编辑:marc_sTheMemeMachine 更新时间:12/3/2021 访问量:540

问:

我正在编写一个模板函数,该函数接受自定义类(可以是任何类或基元类型)作为模板参数,然后从输入流中读取一些数据(该类型),然后将其存储为类似于以下内容的无序映射:

std::unordered_map<CustomClass, std::vector<CustomClass>> map;

我已经实现了一个自定义类来测试该行为。我已经重载了 std::hash,以便该类可以作为键存储在无序映射中,并重载所有运算符和构造函数,以便每当调用它们时,我都会在控制台中收到一条消息(例如,当调用复制构造函数时,我会收到一条消息“复制构造函数 [..数据...]“)

编辑:按照注释中的要求,这里是自定义类的定义和实现(请注意:这里的类只是一个占位符,因此我们可以讨论这个问题背后的一般思想。我很清楚这是愚蠢的,不应该这样实施。>>和<<的运算符代码不在这里,以避免混乱)

class CustomClass {
public:
    CustomClass(int a=0) {
        std::cout << "default constructor" << std::endl;
        m_data = a;
    }

    CustomClass(const CustomClass& other) {
        std::cout << "copy constructor "  ;//<< std::endl;
        m_data = other.m_data;
        std::cout << "[" << m_data << "]"  << std::endl;
    }

    CustomClass(CustomClass&& other) {
        std::cout << "move cosntructor" << std::endl;
        m_data = other.m_data;
    }

    CustomClass& operator=(const CustomClass& other) {
        std::cout << "copy assignment operator" << std::endl;
        if(this != &other){
           m_data = other.m_data;
        }
        return *this;
    }

    CustomClass& operator=(CustomClass&& other) {
        std::cout << "move assignment operator" << std::endl;
        if(this != &other){
            m_data = other.m_data;
        }
        return *this;
    }

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

    int m_data;
};

现在我的问题是:是否可以在没有复制构造函数调用的情况下从输入流中读取数据并在需要的地方就地构造它?

一些代码示例:

CustomClass x1;                        // default constructor call
CustomClass x2;                        // default constructor call
std::cout << "----" << std::endl;
std::cin >> x1 >> x2;                  // my input
std::cout << "----" << std::endl;
map[x1].emplace_back(x2);              // 2 copy constructor calls
std::cout << "----" << std::endl;
std::cout << map[x1][0] << std::endl;  // operator==  call
std::cout << "----" << std::endl;

下面是该代码的示例输出:

default constructor
default constructor
----
[1]
[2]
----
copy constructor [1] 
copy constructor [2]
----
operator ==
[2]
----
destructor
destructor
destructor
destructor

我希望它使此类的每个对象只构造一次。

是否可以避免这些复制构造函数?如果不是两者,那么至少在 emplace_back() 调用期间调用的那个?是否可以在向量中准确地构造对象,使其在内存中需要的位置,但这种调用适用于每种类型?

如果我需要进一步详细说明我的问题,请在评论中告诉我,我很乐意这样做

C++ 输入 复制构造函数

评论

0赞 Brian61354270 12/2/2021
假设这是 C++11 或更高版本,看起来您正在打破五法则。你熟悉移动语义吗?
0赞 TheMemeMachine 12/2/2021
@Brian 我正在使用 C++17。我也重载了移动构造函数和 move = 运算符(但是它们在这里没有调用)。我对移动语义很熟悉,但不是很深入。我知道在指针发挥作用时应该使用移动语义,但是我的代码并非如此,CustomClass 仅存储 3 个整数变量。你愿意详细说明我在这里做错了什么吗?
0赞 Dmitry Kuzminov 12/2/2021
您可以调用并使用从流中读取它。resize()back()
2赞 Brian61354270 12/2/2021
再看一眼,我以为我看到的问题并不存在。我认为唯一的问题是您没有进行可以在 之后过期的通信,因此需要制作副本以绑定emplace_back排除的右值引用。您可以通过使用 来修复此问题。参见 什么是 std::move(),什么时候应该使用它?x2map[x1].emplace_back(x2);map[x1].emplace_back(std::move(x2));
1赞 Brian61354270 12/3/2021
@TheMemeMachine 我认为四个析构函数调用是预期的:一个用于 ,一个用于 ,一个用于映射中的键,一个用于映射中的值。移出的对象仍会调用其析构函数。我不确定为什么您的程序在使用 MSVC 编译时会崩溃,因为它适用于 GCC 和 clang。x1x2

答:

0赞 Dmitry Kuzminov 12/2/2021 #1

所以你有一个,你想在那里放一个元素,避免不必要的复制。假设类的移动构造函数很便宜,第一个选项是定义一个非平凡的构造函数,从流中读取参数,然后从新创建的对象中读取参数:std::vectoremplace_back

using CustomClass = std::vector<int>;

std::vector<CustomClass> v;
size_t size;
int value;
std::cin >> size >> value;
v.emplace_back(size, value);

在这里,我将 定义为整数向量,它有一个构造函数,它接受 2 个参数:size 和 value。当然,读取两个整数并仅创建一次的实例并为此目的使用 更便宜,而不是创建实例并使用:CustomClassCustomClassemplace_backpush_back

using CustomClass = std::vector<int>;

std::vector<CustomClass> v;
size_t size;
int value;
std::cin >> size >> value;
CustomClass instance(size, value);
v.push_back(instance);

然而,与推回 r 值相比,这并没有给你带来太多好处:

using CustomClass = std::vector<int>;

std::vector<CustomClass> v;
size_t size;
int value;
std::cin >> size >> value;
v.push_back(CustomClass(size, value));

无论如何,您需要记住,两者都可能需要重新分配元素,这可能效率低下,尤其是在您没有无投掷移动构造函数的情况下。push_backemplace_backCustomClass

另一个问题可能是,如果你的类没有一个合理的构造函数(或者你需要传递给构造函数的值的大小几乎是对象的大小)。在这种情况下,我为您提供解决方案并阅读resize()back()

如果您不害怕重新分配(例如,您提前知道元素的数量并保留缓冲区),则可以执行以下操作:

std::vector<CustomClass> v;

v.resize(v.size() + 1);
std::cin >> v.back();

在本例中,只需创建一次默认值,然后读取内容。

另一种解决方案可能是将 传递给 的构造函数:std::istreamCustomClass

class CustomClass {
public:
    CustomClass(std::istream&);
};

std::vector<CustomClass> v;
v.emplace_back(cin);

更新:

假设您对 CustomClass 的实际类型一无所知,那么最通用的(不是完全通用的,因为它仍然需要默认构造函数才能推送)是使用 / idiom。resize()resize()back()

0赞 C.M. 12/2/2021 #2

这是你如何做到的(避免任何不必要的 ctor 调用,包括默认调用):

#include <vector>
#include <unordered_map>
#include <cstdio>
#include <iostream>


using namespace std;


//--------------------------------------------------------------------------
template <class F> struct inplacer
{
    F f;
    operator invoke_result_t<F&>() { return f(); }
};

template <class F> inplacer(F) -> inplacer<F>;


//--------------------------------------------------------------------------
struct S
{
    S(istream&) { printf("istream ctor\n" ); }
    S()         { printf("ctor\n" ); }
    ~S()        { printf("dtor\n" ); }
    S(S const&) { printf("cctor\n"); }
    S(S&&)      { printf("mctor\n"); }
    S& operator=(S const&) { printf("cop=\n"); return *this; }
    S& operator=(S&&)      { printf("mop=\n"); return *this; }
    friend bool operator==(S const& l, S const& r) { return &l == &r; } //!! naturally, this needs proper implementation
};

template<> struct std::hash<S>
{
    size_t operator()(S const&) const noexcept { return 0; } //!! naturally, this needs proper implementation
};


//--------------------------------------------------------------------------
template<class R> struct read_impl;   // "enables" partial specialization

template<class R> R read(istream& is)
{
    return read_impl<R>::call(is);
}

template<> struct read_impl<S>
{
    static auto call(istream& is) { return S(is); }
};

template<class T> struct read_impl<vector<T>>
{
    static auto call(istream& is)
    {
        vector<T> r; r.reserve(2);          //!! naturally you'd read smth like length from 'is'
        for(int i = 0; i < 2; ++i)
            r.emplace_back(inplacer{[&]{ return read<T>(is); }});
        return r;
    }
};

template<class K, class V> struct read_impl<unordered_map<K, V>>
{
    static auto call(istream& is)
    {
        unordered_map<K, V> r;
        r.emplace( inplacer{[&]{ return read<K>(is); }}, inplacer{[&]{ return read<V>(is); }} );
        return r;
    }
};


//--------------------------------------------------------------------------
auto m = read<unordered_map<S, vector<S>>>(cin);

正如您在输出中看到的 -- 您最终会得到 3 个“istream ctor”调用和 3 个“dtor”调用。

至于 iostreams——如果你关心性能、清晰度等,请远离它们......有史以来最荒谬的图书馆。

P.S. “函数模板的部分专业化”技巧是从这里偷来的。

评论

0赞 Ben Voigt 12/3/2021
好吧,“3 个 istream ctor 调用”是不正确的,因为流中只有两个对象。事实上,没有办法避免将键读入临时对象,因为在您知道要在映射中添加新条目(而不是附加到现有映射条目的向量)之前,您无法就地构造键,并且直到从流中读取对象后才能知道要添加。
0赞 C.M. 12/3/2021
@BenVoigt,如果你仔细阅读这段代码,你会注意到我们创建了 3 个实例——1 个用于 key,2 个用于 vector
0赞 Ben Voigt 12/3/2021
OP 的代码仅从 istream 中提取键/值对。任何从 istream 读取 3 个对象的“优化”都是不正确的,因为流不包含奇数。另请注意,在 OP 的代码中,可能已经存在,而不是添加新条目。我认为你的代码失去了这种能力......map[x1]
0赞 C.M. 12/3/2021
@BenVoigt 更改我的代码以执行 OP 需要的任何操作是微不足道的。它只是一个演示,说明如何在没有不必要的 (ctor) 调用的情况下从流中提取数据。顺便说一句,afaik,最终将在查找中使用它的密钥之前生成整个节点(如果密钥已经存在,则先丢弃)。map::emplace()