提问人:TheMemeMachine 提问时间:12/2/2021 最后编辑:marc_sTheMemeMachine 更新时间:12/3/2021 访问量:540
是否可以避免在C++中调用复制构造函数
Is it possible to avoid copy-constructor call in C++
问:
我正在编写一个模板函数,该函数接受自定义类(可以是任何类或基元类型)作为模板参数,然后从输入流中读取一些数据(该类型),然后将其存储为类似于以下内容的无序映射:
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() 调用期间调用的那个?是否可以在向量中准确地构造对象,使其在内存中需要的位置,但这种调用适用于每种类型?
如果我需要进一步详细说明我的问题,请在评论中告诉我,我很乐意这样做
答:
所以你有一个,你想在那里放一个元素,避免不必要的复制。假设类的移动构造函数很便宜,第一个选项是定义一个非平凡的构造函数,从流中读取参数,然后从新创建的对象中读取参数: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);
在这里,我将 定义为整数向量,它有一个构造函数,它接受 2 个参数: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 的实际类型一无所知,那么最通用的(不是完全通用的,因为它仍然需要默认构造函数才能推送)是使用 / idiom。resize()
resize()
back()
这是你如何做到的(避免任何不必要的 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. “函数模板的部分专业化”技巧是从这里偷来的。
评论
map[x1]
map::emplace()
评论
resize()
back()
x2
map[x1].emplace_back(x2);
map[x1].emplace_back(std::move(x2));
x1
x2