我最近开始在MSVC的实现中检查STL。那里有一些不错的技巧,但我不知道为什么使用以下标准。
如果符合某些条件,则std::uninitialized_copy
会针对简单memcpy/memmove
进行优化。根据我的理解,如果源类型为T的目标类型为U memcpy
,则输入范围可以是is_trivially_copy_constructible
'到未初始化区域。
然而,在选择memcpy
而不是逐个复制构造元素之前,MSVC实现检查了很多东西。我不想在这里粘贴相关代码,而是我通过pastebin分享它,如果有人感兴趣的话:https://pastebin.com/Sa4Q7Qj0
uninitialized_copy
的基本算法是这样的(可读性省略了异常处理)
template <typename T, typename... Args>
inline void construct_in_place(T& obj, Args&&... args)
{
::new (static_cast<void*>(addressof(obj)) T(forward<Args>(args)...);
}
template <typename In, typename Out>
inline Out uninitialized_copy(In first, In last, Out dest)
{
for (; first != last; ++first, ++dest)
construct_in_place(*dest, *first);
}
如果复制构造不执行任何“特殊”操作(可以简单地复制构造),则可以优化为memcpy/memmove
。
MS的实施需要以下内容:
例如,以下结构不能是memcpy
'd:
struct Foo
{
int i;
Foo() : i(10) { }
};
但以下是可以的:
struct Foo
{
int i;
Foo() = default; // or simply omit
};
检查类型U是否可以从类型T简单地复制构造是否应该足够?因为所有这些都是uninitialized_copy。
例如,我无法理解为什么MS的STL实现不会记忆以下内容(注意:我知道原因,它是用户定义的构造函数,但我不明白它背后的逻辑):
struct Foo
{
int i;
Foo() noexcept
: i(10)
{
}
Foo(const Foo&) = default;
};
void test()
{
// please forgive me...
uint8 raw[256];
Foo* dest = (Foo*)raw;
Foo src[] = { Foo(), Foo() };
bool b = std::is_trivially_copy_constructible<Foo>::value; // true
bool b2 = std::is_trivially_copyable<Foo>::value; // true
memcpy(dest, src, sizeof(src)); // seems ok
// uninitialized_copy does not use memcpy/memmove, it calls the copy-ctor one-by-one
std::uninitialized_copy(src, src + sizeof(src) / sizeof(src[0]), dest);
}
相关SO帖子:Why doesn't gcc use memmove in std::uninitialized_copy?
正如@Igor Tandetnik在评论中指出的那样,如果没有用户定义的拷贝构造函数那么说是不安全的,那么类型T可以简单地复制构造。他提供了以下例子:
struct Foo
{
std::string data;
};
在此示例中,没有用户定义的复制构造函数,并且它仍然不是简单的可复制构造。感谢您的更正,我根据反馈修改了原帖。
答案 0 :(得分:2)
uninitialized_copy
有两个职责:首先,它必须确保正确的位模式进入目标缓冲区。其次,它必须开始该缓冲区中C ++对象的生存期。也就是说,它必须调用某种构造函数,除非C ++ Standard专门授予它跳过该构造函数调用的权限。
根据我非常不完整的研究,看来现在只有trivially copyable类型可以保证由memcpy
/ memmove
保留其位模式;记忆任何其他类型的类型(即使碰巧是普通的可复制构造的和/或普通的可复制的!)也会产生不确定的行为。
此外,现在似乎只有trivial类型可以在没有构造函数调用的情况下“出现”。 (P0593 "Implicit creation of objects..."在此领域提出了许多更改,也许在C ++ 2b中。)
Jonathan Wakely对libstdc++ bug 68350的评论似乎表明,GNU libstdc ++试图通过从未出现任何非平凡类型的对象(即使作为C ++实现)也不会“冒出来”,从而试图保留在法律之内。 ,他们确实可以以性能的名义利用平台特定的行为。我想出于类似的原因(无论这些原因是什么),MSVC也会遵循类似的逻辑。
通过比较供应商对“普通复制但不琐碎”的类类型优化std::copy
和std::uninitialized_copy
的意愿,可以看到供应商不愿意“弹出对象”。普通复制意味着std::copy
可以使用memcpy
来分配现有对象;但是std::uninitialized_copy
,要使这些对象首先弹出来,仍然感到需要循环调用 some 构造函数-即使它是琐碎的复制构造函数!
class C { int i; public: C() = default; };
class D { int i; public: D() {} };
static_assert(std::is_trivially_copyable_v<C> && !std::is_aggregate_v<C>);
static_assert(std::is_trivially_copyable_v<D> && !std::is_aggregate_v<D>);
void copyCs(C *p, C *q, int n) {
std::copy(p, p+n, q); // GNU and MSVC both optimize
std::uninitialized_copy(p, p+n, q); // GNU and MSVC both optimize
}
void copyDs(D *p, D *q, int n) {
std::copy(p, p+n, q); // GNU and MSVC both optimize
std::uninitialized_copy(p, p+n, q); // neither GNU nor MSVC optimizes :(
}
您写道:
检查U型是否可以从T型简单复制构造就足够了吗?因为这就是uninitialized_copy的全部。
是的,但是当T和U 不同时,您不是在进行“琐碎的复制构建”;您正在执行的“琐碎构造”不是 复制构造。不幸的是,C ++标准将is_trivially_constructible<T,U>
定义为不同于人类“琐碎”的含义!我的博客文章"Trivially-constructible-from" (July 2018)给出了以下示例:
assert(is_trivially_constructible_v<u64, u64b>);
// Yay!
using u16 = short;
assert(is_trivially_constructible_v<u64, u16>);
// What the...
assert(is_trivially_constructible_v<u64, double>);
// ...oh geez.
这解释了MSVC的一些
如果T!= U,则进行额外检查(如sizeof(T)== sizeof(U))
具体来说,MSVC的_Ptr_cat_helper<T*,U*>::_Really_trivial
特性依靠那些额外的检查来检测某些(但不是全部)常见的情况,其中从T到U的转换在人/位意义上“确实”是微不足道的,而不仅仅是微不足道的在C ++标准意义上。这使MSVC可以优化将int*
数组复制到const int*
数组中,这是libstdc ++无法做到的:
using A = int*;
using B = const int*;
void copyAs(A *p, B *q, int n) {
std::uninitialized_copy(p, p+n, q); // only MSVC optimizes
}
void copyBs(B *p, B *q, int n) {
std::uninitialized_copy(p, p+n, q); // GNU and MSVC both optimize
}