哪种类型特征表明该类型可以进行memcpy赋值?(元组、对)

10
我想知道可以进行哪种内省来检测可以通过简单的原始内存复制进行赋值的类型?
例如,据我所了解,内置类型、内置类型元组和此类元组的元组都属于此类别。动机是我希望在可能的情况下传输原始字节。
T t1(...); // not necessarely default constructible 
T t2(...);

t1 = t2; // should be equivalent to std::memcpy(&t1, &t2, sizeof(T));
// t1 is now an (independent) copy of the value of t2, for example each can go out of scope independently
什么type_trait或者type_traits的组合可以在编译时告诉我们是否可以使用memcpy替换赋值操作?

我尝试了适用于我猜测应该满足此条件的类型,并且令我惊讶的是,唯一符合行为的选项不是std::is_trivially_assignable而是std::trivially_destructible。某种程度上来说这是有道理的,但我感到困惑的是一些其他选项在预期的情况下为何不起作用。

我知道可能没有绝对可靠的方法,因为总是可以编写一个实际上是可以通过memcopy进行拷贝的类,但无法被"检测"为可拷贝,但我正在寻找适用于简单直观情况的方法。

#include<type_traits>
template<class T> using trait = 
    std::is_trivially_destructible
//  std::is_trivial
//  std::is_trivially_copy_assignable
//  std::is_trivially_copyable // // std::tuple<double, double> is not trivially copyable!!!
//  std::is_trivially_default_constructible
//  std::is_trivially_default_constructible
//  std::is_trivially_constructible
//  std::is_pod // std::tuple<double, double> is not pod!!!
//  std::is_standard_layout
//  std::is_aggregate
//  std::has_unique_object_representations
    <T>
;

int main(){
    static_assert((trait<double>{}), "");
    static_assert((trait<std::tuple<double, double>>{}), "");
    static_assert((not trait<std::tuple<double, std::vector<double>>>{}), "");
    static_assert((not trait<std::vector<double>>{}), "");
}

当然,我认为元组应该是可复制的,并不是基于标准,而是基于常识和实践。也就是说,因为这通常是可以的:

std::tuple<double, std::tuple<char, int> > t1 = {5.1, {'c', 8}};
std::tuple<double, std::tuple<char, int> > t2;
t2 = t1;
std::tuple<double, std::tuple<char, int> > t3;
std::memcpy(&t3, &t1, sizeof(t1));
assert(t3 == t2);

作为原理证明,我实现了这个功能。我添加了几个与大小相关的条件,以避免一些可能会导致std::tuple特化误导的情况。
template<class T> 
struct is_memcopyable 
: std::integral_constant<bool, std::is_trivially_copyable<T>{}>{};

template<class T, class... Ts> 
struct is_memcopyable<std::tuple<T, Ts...>> : 
    std::integral_constant<bool, 
        is_memcopyable<T>{} and is_memcopyable<std::tuple<Ts...>>{}
    >
{};

template<class T1, class T2> 
struct is_memcopyable<std::pair<T1, T2>> : 
    std::integral_constant<bool, 
        is_memcopyable<T1>{} and is_memcopyable<T2>{}
    >
{};

这只是一个非常有限的解决方案,因为像以下这样的类:

struct A{ std::tuple<double, double> t; }; 

遗憾的是,它仍然会被报告为非平凡可复制和非内存复制。


1
虽然这并不是回答你的问题,但它不必仅仅是一个单一的type_trait。也许可以编写自己的组合traits。例如:(is_pod<T>::value || is_trivially_assignable<T>::value) - Joe
2
在这种情况下,您认为元组应该是平凡可分配的信念的来源是什么? 这个答案似乎并不是这样说的: https://dev59.com/vlkT5IYBdhLWcg3wT91f - Joe
1
你可以创建自己的类型特征,为 std 或库或用户定义的类型创建尽可能多的专门化,如果需要,可以回退到 std type_traits 中的某些内容,即使它们可能会有错误的负面影响。(这是一条注释,因为这不是一个答案。) - davidbak
1
我认为问题在于std::pair的赋值运算符没有默认值...所以也许你应该建议标准修复这个问题...同时,即使它可能按预期工作,我会尽量避免依赖UB。 - Phil1970
1
@alfC:具有非静态引用成员的类型默认将赋值运算符设置为已删除。 Pair 分配其成员的目标对象,而不是引用本身(因为没有引用重新赋值这样的事情),但此行为是非默认的。 - Ben Voigt
显示剩余14条评论
3个回答

11

正确的测试实际上是std::is_trivially_copyable,它允许使用memcpy来创建新对象和修改现有对象。

尽管您可能会对这些类型返回false感到惊讶,因为您的直觉告诉您memcpy应该没问题,但它们并没有说谎;标准确实在这些情况下使memcpy成为未定义行为。


在特定情况下,对于std::pair,我们可以了解一些出错的原因:

int main()
{
    typedef std::pair<double,double> P;
    std::cout << "\nTC:  " << std::is_trivially_copyable<P>::value;
    std::cout << "\nTCC: " << std::is_trivially_copy_constructible<P>::value;
    std::cout << "\nTCv: " << std::is_trivially_constructible<P, const P&>::value;
    std::cout << "\n CC: " << std::is_copy_constructible<P>::value;
    std::cout << "\n MC: " << std::is_move_constructible<P>::value;
    std::cout << "\nTCA: " << std::is_trivially_copy_assignable<P>::value;
    std::cout << "\nTCvA:" << std::is_trivially_assignable<P, const P&>::value;
    std::cout << "\n CA: " << std::is_copy_assignable<P>::value;
    std::cout << "\n MA: " << std::is_move_assignable<P>::value;
    std::cout << "\nTD:  " << std::is_trivially_destructible<P>::value;
}

TC: 0 TCC: 1 TCv: 1 CC: 1 MC: 1 TCA: 0 TCvA: 0 CA: 1 MA: 1 TD: 1

显然这不是一个简单的可复制赋值操作。1

pair 赋值运算符是用户定义的,因此不是简单的。


1我认为clang、gcc和msvc都是错误的,但如果它满足 std::_is_trivially_copy_assignable,也没有帮助,因为 TriviallyCopyable 要求复制构造函数(如果未删除)是 trivial 的,而不是 TriviallyCopyAssignable 特性。是的,它们是不同的。

如果类X的复制/移动赋值运算符不是用户提供的,则其为 trivial...

is_assignable_v<T, const T&> 为true,并且根据 is_assignable 的定义,已知调用的赋值运算不包含非 trivial 操作。

pair<double, double> 的复制赋值运算符调用的操作是两个 double 的赋值,这些操作是 trivial 的。

不幸的是,trivially copyable 的定义依赖于第一个条件,而 pair 不符合。

一个 trivially copyable 类是一个类:

  • 每个复制构造函数、移动构造函数、复制赋值运算符和移动赋值运算符都是已删除或 trivial 的。
  • 具有至少一个未删除的复制构造函数、移动构造函数、复制赋值运算符或移动赋值运算符,且
  • 具有 trivial、未删除的析构函数。

@alfC: std::pair具有用户定义的复制赋值运算符,这已经足以破坏平凡的可复制性。 - Ben Voigt
1
@alfC:相关链接:https://groups.google.com/a/isocpp.org/forum/#!topic/std-discussion/PUZ9WUr2AOU - Ben Voigt
1
@alfC:这里有一个讨论,似乎唯一的原因是为了向后兼容性而定义用户:https://groups.google.com/a/isocpp.org/forum/#!msg/std-proposals/Zi2wriFMFvI/N1JlOE-FBQAJ - Ben Voigt
好的,我认为这毕竟是实现上的缺陷。另外,顺便提一下,我可以使用is_trivially_destructible。我很难想象一个平凡可析构类不可memcopyable。 - alfC
1
@alfC:是的,如果元素类型是的话,std::complex 是可以平凡复制的,std::array 也是。它们没有额外特殊的酱汁来支持 tie 的使用。 - Ben Voigt
显示剩余8条评论

4
为了回答你的问题:std::memcpy()没有直接要求,但它有以下规定:
将count个字节从src指向的对象复制到dest指向的对象。两个对象都被重新解释为unsigned char数组。
如果对象重叠,则行为未定义。
如果dest或src是空指针,则行为未定义,即使count为零。
如果对象不是TriviallyCopyable,则memcpy的行为未指定,并且可能未定义。
现在,要使对象成为Trivially Copyable,必须满足以下条件或要求:
每个复制构造函数都是平凡的或已删除。
每个移动构造函数都是平凡的或已删除。
每个复制赋值运算符都是平凡的或已删除。
每个移动赋值运算符都是平凡的或已删除。
至少有一个复制构造函数、移动构造函数、复制赋值运算符或移动赋值运算符是未删除的。
平凡的未删除析构函数。
这意味着该类没有虚函数或虚基类。
标量类型和TriviallyCopyable对象的数组也是TriviallyCopyable的,以及此类类型的const限定(但不是volatile限定)版本。
这就引出了std::is_trivially_copyable:
如果T是TriviallyCopyable类型,则提供成员常量value等于true。对于任何其他类型,value为false。
唯一的Trivially Copyable类型是标量类型、Trivially Copyable类和这些类型/类的数组(可能是const限定,但不是volatile限定)。
如果std::remove_all_extents_t是不完整的类型且不是(可能是cv限定的)void,则行为未定义。
自C++17以来,具有此好特性:

Helper variable template

template< class T >
inline constexpr bool is_trivially_copyable_v = is_trivially_copyable<T>::value; 

而您想尝试使用type_trait来使用std::tuple<>std::memcpy()。但我们需要问自己,为什么std::tuple不是Trivially Copyable?我们可以在这里找到答案:Stack-Q/A: std::tuple Trivially Copyable?根据那个答案,它不是因为标准没有要求复制/移动赋值运算符是平凡的。所以我认为有效的答案应该是:不,std::tuple不是Trivially Copyable,但std::memcpy()并不要求它是,只是声明如果不是,则为UB。那么你能用std::tuplestd::memcpy吗?我认为可以,但是安全吗?可能会有风险,并且可能会产生UB。那么我们从这里能做什么呢?冒险吗?也许。我发现了另一件相关的事情,但还没有找到关于它是否是Trivially Copyable的任何信息。它不是一个type_trait,但它是可能能够与std::tuplestd::memcpy一起使用的东西,那就是std::tuple_element。您可能可以使用它来执行memcpy,但我不太确定。我已经搜索了更多关于std::tuple_element的信息,以了解它是否是Trivially Copyable,但没有找到太多信息,所以我只能进行测试以查看Visual Studio 2017的结果:
template<class... Args>
struct type_list {
    template<std::size_t N>
    using type = typename std::tuple_element<N, std::tuple<Args...>>::type;
};

int main() {
    std::cout << std::boolalpha;
    std::cout << std::is_trivially_copyable<type_list<int, float, float>>::value << '\n';
    std::cout << std::is_trivially_copyable<std::tuple<int, float, float>>::value << '\n';

    _getch(); // used to stop visual studio debugger from closing.
    return 0;
}

输出:

true
false

因此,似乎如果我们将std::tuple_element包装在一个结构体中,它就是可平凡复制的。现在的问题是如何将其与您的std::tuple数据集集成,以便使用std::memcpy()使其类型安全。不确定是否可以,因为std::tuple_element将返回tuple内元素的类型。
如果我们尝试这样包装一个tuple
template<class... Args>
struct wrapper {
    std::tuple<Args...> t;
};

我们可以通过以下方式进行检查:

{
    std::cout << std::is_trivially_copyable< wrapper<int, float, float> >::value << std::endl;
}

它仍然是false。然而,我们已经看到了在第一个结构体中已经使用了std::tuple,并且该结构体返回了true。这可能对您有所帮助,以确保您可以安全地使用std::memcpy,但我不能保证。只是编译器似乎同意它。因此,这可能是最接近可行的type_trait的东西。
注意:- 所有关于memcpyTrivially Copyable conceptsis_trivially_copyablestd::tuplestd::tuple_element的引用都来自于cppreference及其相关页面。

3
我不明白。tuple_element 用于获取索引的类型,它不保存任何值,这就是为什么它是平凡的。 - alfC
@alfC 是的,我不熟悉它。我不知道它是否可以使用。 - Francis Cugler
@alfC,我觉得我回答的结尾措辞不对;因为OP正在寻找一种“type_trait”,这可能有助于他问题的解决。 - Francis Cugler

4

这只是对你问题的部分回答:

类型特质不一定字面意义上表示其名称。

具体来说,让我们看看std::is_trivially_copyable。你很正确地惊讶于一个由两个double组成的元组不是平凡可复制的。怎么会这样?!

好吧,trait definition说:

如果T是一个TriviallyCopyable类型,则提供成员常量value等于true。对于任何其他类型,value都是false

TriviallyCopyable concept在其定义中有以下要求:

  • 每个复制构造函数都是平凡的或已删除
  • 每个移动构造函数都是平凡的或已删除
  • 每个复制赋值运算符都是平凡的或已删除
  • 每个移动赋值运算符都是平凡的或已删除
  • 至少有一个复制构造函数、移动构造函数、复制赋值运算符或移动赋值运算符是未删除的
  • 平凡未删除的析构函数

这可能不是您所期望的,对吧?

考虑到所有这些,标准库特性的任何组合都不一定能满足“通过 memcpy() 进行构造”的确切要求。


1
明白了,trivial 的意思是它什么也不做,而不是做一些简单的事情。我想唯一的希望就是 is_pod 和/或 is_aggregate。但这似乎不太可能奏效。我想我会递归地实现一些基于特性的自定义特性,因为 memcopyable 的元组本身应该是 memcopyable 的。 - alfC
1
@alfC:我认为is_pod 在C++20中将被废弃 - einpoklum
2
@einpoklum:不,TriviallyCopyable是标准对使用带有memcpy的类型所提出的条件。反直觉的是,不能使用memcpy与该对一起使用,而不是std::is_trivially_copyable中的任何错误行为。 - Ben Voigt
1
@einpoklum:就是标准没有定义在任何不是TriviallyCopyable类型上使用memcpy的行为。 "对于任何可平凡复制的类型T,如果两个指向T对象obj1和obj2的指针指向不同的T对象,其中obj1和obj2都不是基类子对象,则如果将组成obj1的底层字节复制到obj2中,obj2随后应该持有与obj1相同的值。" - Ben Voigt
1
另外,我不确定你认为 std::pair<double,double> 应该失败的是哪个测试。TriviallyCopyable 只会限制复制和移动构造函数,而你提到的 "极其丰富的集合" 是完全无关紧要的。 - Ben Voigt
显示剩余7条评论

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接