【问题标题】:Is it possible to avoid copy-constructor call in C++是否可以避免在 C++ 中调用复制构造函数
【发布时间】:2022-01-08 12:41:34
【问题描述】:

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

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

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

编辑:根据 cmets 的要求,这里是自定义类的定义和实现(请注意:这里的类只是一个占位符,所以我们可以讨论这个问题背后的总体思路。我是很清楚这是愚蠢的,不应该这样实现。运算符>>和

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() 调用期间调用的那个?是否可以在向量中准确地在内存中需要的位置构造对象,但这种调用适用于每种类型?

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

【问题讨论】:

  • 假设这是 C++11 或更高版本,看起来你正在破坏the rule of five。你熟悉move semantics吗?
  • @Brian 我正在使用 C++17。我也重载了 move 构造函数和 move = 运算符(但是这里没有调用它们)。我对移动语义很熟悉,但不是很深入。我知道在播放指针时应该使用移动语义,但是我的代码并非如此,CustomClass 只存储 3 个整数变量。您愿意详细说明我在这里做错了什么吗?
  • 您可以调用resize()并使用back()从流中读取。
  • 再看一遍,我以为我看到的问题不存在。我认为唯一的问题是您没有传达x2 可以在map[x1].emplace_back(x2); 之后过期,因此需要制作一个副本以便与emplace_back 例外的右值引用绑定。您可以使用map[x1].emplace_back(std::move(x2)); 来解决这个问题。另见What is std::move(), and when should it be used?
  • @TheMemeMachine 我认为应该有四个析构函数调用:一个用于x1,一个用于x2,一个用于映射中的键,一个用于映射中的值。移出的对象仍然调用它们的析构函数。我不确定为什么你的程序在使用 MSVC 编译时会崩溃,因为它同时适用于 GCC 和 clang。

标签: c++ input constructor copy-constructor


【解决方案1】:

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

using CustomClass = std::vector<int>;

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

在这里,我将CustomClass 定义为一个整数向量,它有一个带有两个参数的构造函数:大小和值。当然,读取两个整数并只创建一次CustomClass 的实例并为此使用emplace_back 会更便宜,而不是创建实例并使用push_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_back 和 emplace_back 都可能需要重新分配元素,这可能效率低下,特别是如果您的 CustomClass 没有禁止投掷移动构造函数。

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

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

std::vector<CustomClass> v;

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

在这种情况下,您创建一次默认值,然后读取内容。

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

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

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

更新:

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

【讨论】:

    【解决方案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);
    

    正如您在 the output 中看到的那样——您最终会收到 3 个“istream ctor”调用和 3 个“dtor”调用。

    至于 iostream——如果您关心性能、清晰度等,请远离它们......有史以来最荒谬的库。

    附: “函数模板的部分特化”技巧是从here中窃取的。

    【讨论】:

    • 好吧,“3 istream ctor calls”是不正确的,因为流中只有两个对象。事实上,没有办法避免将密钥读入临时对象,因为在您知道要在地图中添加新条目之前,您无法就地构造密钥(而不是附加到现有地图条目的向量中) ),直到从流中读取对象之后,您才能知道自己正在添加。
    • @BenVoigt 如果您仔细阅读该代码,您会注意到我们创建了 3 个实例——1 个用于键,2 个用于矢量
    • OP 的代码只从 istream 中提取键/值对。从 istream 读取 3 个对象的任何“优化”都是不正确的,因为该流不包含奇数。另请注意,在 OP 的代码中,map[x1] 可能已经存在,而不是添加新条目。我认为你的代码失去了这种能力......
    • @BenVoigt 更改我的代码以执行 OP 需要的任何操作都是微不足道的。这只是一个演示如何从流中提取数据而无需不必要的 (ctor) 调用。顺便说一句,afaik,map::emplace() 将最终生成整个节点,然后在查找中使用它的键(如果键已经存在,则随后丢弃)。
    猜你喜欢
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    相关资源
    最近更新 更多