Search code examples
c++lifetimememcpy

Meaning of "trivial eligible constructor" for implicit lifetime types and memcpy


Say you have a type that is trivially copyable, not an aggregate, and not trivially constructible:

struct Foo
{
    Foo() = default;

    Foo(int i)
    : a(i)
    {};

    int a = 5;
};

Foo isn't an aggregate because it has a user-declared constructor, it isn't trivially constructible because it has user-defined initialisers, and it is trivially copyable and trivially destructible. It isn't a trivial type.

Is it legal to attempt to implicitly construct such a type via memcpy?

Foo* makeFooCopy(const Foo& src)
{
    // Assume alignment isn't a problem
    auto ptr = malloc(sizeof(Foo));

    memcpy(ptr, &src, sizeof(Foo));

    return reinterpret_cast<Foo*>(ptr);
}

cppreference says that an implicit lifetime type "...has at least one trivial eligible constructor and a trivial, non-deleted destructor." (the aggregate case does not apply here). But it's not clear to me what the "trivial eligible constructor" is here; must it be a default constructor (i.e. this is just stating that the type needs to be trivially default constructible) or is the ability to trivially copy the object sufficient?

The motivating issue is a vector-like type in our code; profiling shows that in a specific use case a significant amount of our run time consists of copying contiguous containers of trivially copyable but not trivially default constructible types into our vector-like type, which is currently implemented as a loop around emplace_back. We would like to just use memcpy to copy the entire buffer, like so:

template<MemcpyableContainer C>
SpecialVectorType(const C& container)
{
    resize(std::size(container));

    memcpy(our_storage, std::addressof(*std::begin(container)), std::size(container) * sizeof(element_type))
}

but our compiler isn't optimising out the placement new calls in resize. It's not clear to me if it's legal to elide them.


Solution

  • "At least one trivial eligible constructor" means any of the three constructors(default constructor/copy constructor/move constructor) is trivial.

    The type trait std::is_implicit_lifetime is added in C++23.

    Here is the implementation in the related draft p2674

    template <typename T>
    struct is_implicit_lifetime
        : std::disjunction<
              std::is_scalar<T>, std::is_array<T>, std::is_aggregate<T>,
              std::conjunction<
                  std::is_trivially_destructible<T>,
                  std::disjunction<std::is_trivially_default_constructible<T>,
                                   std::is_trivially_copy_constructible<T>,
                                   std::is_trivially_move_constructible<T>>>> {};
    
    

    There's no doubt class Foo is an implicit-lifetime type. It is well defined to implicitly create such objects via memcpy.