利用容器的移动语义和元素初始化
Utilizing move-semantics and element-wise initialization of containers
经常看到使用 STL 算法的示例用 list-initialized 容器来说明,例如:
std::vector< int > v{1, 2, 3, 4};
但是当这种方法用于(重量级)类(不像int
s)时,它意味着过度的复制操作,即使他们由 rvalue 传递( 移至 ),因为上例中使用的 std::initializer_list
仅提供 const_iterator
s.
为了解决这个问题,我使用以下 (C++17) 方法:
template< typename Container, typename ...Args >
Container make_container(Args &&... args)
{
Container c;
(c.push_back(std::forward< Args >(args)), ...);
// ((c.insert(std::cend(c), std::forward< Args >(args)), void(0)), ...); // more generic approach
return c;
}
auto u = make_container< std::vector< A > >(A{}, A{}, A{});
但是当我执行以下操作时,它变得不令人满意:
A a;
B b;
using P = std::pair< A, B >;
auto v = make_container< std::vector< P > >(P{a, b}, P{std::move(a), std::move(b)});
这里我想通过移动操作替换复制操作来为每个值保存一个复制操作(假设移动 A
或 B
比复制便宜得多) , 但通常不能,因为函数参数的求值顺序在 C++ 中未定义。我目前的解决方案是:
template< Container >
struct make_container
{
template< typename ...Args >
make_container(Args &&... args)
{
(c.push_back(std::forward< Args >(args)), ...);
}
operator Container () && { return std::move(c); }
private :
Container c;
};
A a; B b;
using P = std::pair< A, B >;
using V = std::vector< P >;
V w = make_container< V >{P{a, b}, P{std::move(a), std::move(b)}};
在构造函数体中做一些重要的工作通常被认为是一种不好的做法,但在这里我大量使用了 list-initialization[=38= 的属性 ] — 事实上它是严格从左到右排序的。
从某些特定的角度来看,这是完全错误的做法吗?除了上述方法之外,这种方法还有哪些缺点?目前是否有另一种技术可以实现函数参数求值的可预测顺序(在 C++11、C++14、C++1z 中)?
Is it totally wrong approach from some particular point of view?
如果被调用的函数在复制之前结束移动,那么理解起来会不必要地困难并且会严重中断。请记住,std::move
不会移动。它启用 移动。而已。最终移动的是被调用的函数。如果它 从左到右处理参数,它将起作用。没有就没有。
说清楚是怎么回事。
A a; A b;
using V = std::vector< A >;
A c {a};
V v = make_container< V >{std::move(a), b, std::move(c)};
有更好的解决方案:
template<class Container, std::size_t N>
inline Container make_container(typename Container::value_type (&&a)[N])
{
return Container(std::make_move_iterator(std::begin(a)), std::make_move_iterator(std::end(a)));
}
你可以这样使用它:
make_container<std::vector<A>>({A(1), A(2)})
它不需要可变参数模板,它仍然是列表初始化但不是 std::initializer_list
,这次它是一个普通数组,因此您可以从中移动元素。
与您的原始解决方案相比的显着优势:
- 更好的性能:它直接调用
Container
的ctor可以提供更好的性能(例如std::vector
可以保留所有需要的内存)
- Eval顺序保证:是列表初始化
经常看到使用 STL 算法的示例用 list-initialized 容器来说明,例如:
std::vector< int > v{1, 2, 3, 4};
但是当这种方法用于(重量级)类(不像int
s)时,它意味着过度的复制操作,即使他们由 rvalue 传递( 移至 ),因为上例中使用的 std::initializer_list
仅提供 const_iterator
s.
为了解决这个问题,我使用以下 (C++17) 方法:
template< typename Container, typename ...Args >
Container make_container(Args &&... args)
{
Container c;
(c.push_back(std::forward< Args >(args)), ...);
// ((c.insert(std::cend(c), std::forward< Args >(args)), void(0)), ...); // more generic approach
return c;
}
auto u = make_container< std::vector< A > >(A{}, A{}, A{});
但是当我执行以下操作时,它变得不令人满意:
A a;
B b;
using P = std::pair< A, B >;
auto v = make_container< std::vector< P > >(P{a, b}, P{std::move(a), std::move(b)});
这里我想通过移动操作替换复制操作来为每个值保存一个复制操作(假设移动 A
或 B
比复制便宜得多) , 但通常不能,因为函数参数的求值顺序在 C++ 中未定义。我目前的解决方案是:
template< Container >
struct make_container
{
template< typename ...Args >
make_container(Args &&... args)
{
(c.push_back(std::forward< Args >(args)), ...);
}
operator Container () && { return std::move(c); }
private :
Container c;
};
A a; B b;
using P = std::pair< A, B >;
using V = std::vector< P >;
V w = make_container< V >{P{a, b}, P{std::move(a), std::move(b)}};
在构造函数体中做一些重要的工作通常被认为是一种不好的做法,但在这里我大量使用了 list-initialization[=38= 的属性 ] — 事实上它是严格从左到右排序的。
从某些特定的角度来看,这是完全错误的做法吗?除了上述方法之外,这种方法还有哪些缺点?目前是否有另一种技术可以实现函数参数求值的可预测顺序(在 C++11、C++14、C++1z 中)?
Is it totally wrong approach from some particular point of view?
如果被调用的函数在复制之前结束移动,那么理解起来会不必要地困难并且会严重中断。请记住,std::move
不会移动。它启用 移动。而已。最终移动的是被调用的函数。如果它 从左到右处理参数,它将起作用。没有就没有。
说清楚是怎么回事。
A a; A b;
using V = std::vector< A >;
A c {a};
V v = make_container< V >{std::move(a), b, std::move(c)};
有更好的解决方案:
template<class Container, std::size_t N>
inline Container make_container(typename Container::value_type (&&a)[N])
{
return Container(std::make_move_iterator(std::begin(a)), std::make_move_iterator(std::end(a)));
}
你可以这样使用它:
make_container<std::vector<A>>({A(1), A(2)})
它不需要可变参数模板,它仍然是列表初始化但不是 std::initializer_list
,这次它是一个普通数组,因此您可以从中移动元素。
与您的原始解决方案相比的显着优势:
- 更好的性能:它直接调用
Container
的ctor可以提供更好的性能(例如std::vector
可以保留所有需要的内存) - Eval顺序保证:是列表初始化