#include <type_traits>
#include <cstring>
template<class T> struct unaligned {
using type = T;
// static_assert(std::is_trivially_copyable<type>{}, "");
template<typename... Args> unaligned(Args&&... args) {
const type tmp(std::forward<Args>(args)...);
memcpy(&base, &tmp, sizeof(type));
}
unaligned& operator =(const unaligned &a) {
memcpy(&base, &a.base, sizeof(type));
return *this;
}
operator type() const {
type ret;
memcpy(&ret, &base, sizeof(type));
return ret;
}
private:
type base;
};
struct foo {
char a;
unaligned<int> b;
} __attribute__((packed));
struct bar {
short a;
unaligned<int> b;
unaligned<unaligned<int>> c;
unaligned<unaligned<unaligned<int>>> d;
unaligned<unaligned<int>*> e;
} __attribute__((packed));
//static_assert(std::is_trivially_copyable<unaligned<int>>{}, "");
//static_assert(std::is_trivially_copyable<foo>{}, "");
//static_assert(std::is_trivially_copyable<bar>{}, "");
static foo foo_1;
static bar bar_1;
int test() {
foo_1.b = 1;
int c = foo_1.b;
bar_1.b = foo_1.b;
int d = bar_1.b;
bar_1.c = 2;
bar_1.d = 3;
bar_1.e = &bar_1.b;
*bar_1.e = 3;
return c + d;
}
1. Has no non-trivial copy constructors (this also requires no virtual functions or virtual bases)
2. Has no non-trivial move constructors
3. Has no non-trivial copy assignment operators
4. Has no non-trivial move assignment operators
5. Has a trivial destructor
It seems like 1, 2, 3 and 4 are too much in this situation.std::vector and such use std::is_trivially_copyable to determine whether to use std::memcpy or std::memmove rather than use standard copy constructors, move constructors, placement operator new and direct destructor invocation. If you have a class that has a constructor that initializes its members to have pointers into itself, but that class doesn't have a destructor because it doesn't have any resources to free, std::vector would cause that class to blow up on a resize operation.
I think that there ought to be a distinction between "implementation-compatible with memcpy" and "trivially-copyable". The former is what you are alluding to. If a non-trivially-copiable class that otherwise meets the requirements you mentioned decides that it is OK *at a certain moment*, it ought to be able to memcpy itself. Right now, though it will work in the majority of implementations if you adhere to those requirements, it is technically undefined behavior to memcpy a non-trivially-copyable class.
Another thread of mine proposed adding a [[trivially_copyable]] attribute to allow a class with a copy constructor to declare that despite that copy constructor, the compiler and classes like std::vector could use std::memcpy if they so chose. The proposal was received poorly.
Melissa
Trivially-copiable isn't just for memcpy layout consistency; it's also a marker indicating that the class does not do anything that would break if the object were moved without a constructor. Just because the type has a trivial destructor doesn't mean that it is safe to copy them around arbitrarily. For example, what if one of the members of the class has a pointer to one of the member elements?std::vector and such use std::is_trivially_copyable to determine whether to use std::memcpy or std::memmove rather than use standard copy constructors, move constructors, placement operator new and direct destructor invocation. If you have a class that has a constructor that initializes its members to have pointers into itself, but that class doesn't have a destructor because it doesn't have any resources to free, std::vector would cause that class to blow up on a resize operation.
I think that there ought to be a distinction between "implementation-compatible with memcpy" and "trivially-copyable". The former is what you are alluding to. If a non-trivially-copiable class that otherwise meets the requirements you mentioned decides that it is OK *at a certain moment*, it ought to be able to memcpy itself. Right now, though it will work in the majority of implementations if you adhere to those requirements, it is technically undefined behavior to memcpy a non-trivially-copyable class.
Another thread of mine proposed adding a [[trivially_copyable]] attribute to allow a class with a copy constructor to declare that despite that copy constructor, the compiler and classes like std::vector could use std::memcpy if they so chose. The proposal was received poorly.
They are currently done for things like std::hash. I've seen it done
often for traits like iterator_traits and numeric_traits. There's even
a proposal (N3867) to make it easier to specialize traits templates in
other namespaces with the std:: namespace being the motivating
example. :)
--
---
You received this message because you are subscribed to the Google Groups "ISO C++ Standard - Discussion" group.
To unsubscribe from this group and stop receiving emails from it, send an email to std-discussio...@isocpp.org.
To post to this group, send email to std-dis...@isocpp.org.
Visit this group at http://groups.google.com/a/isocpp.org/group/std-discussion/.
I don't think there's a way around that. The standard cannot know what types
can be memcpy'ed or not based on the members of that structure. The user has
to say something.
This dilemma is probably one of the reasons facebook (and I'm sure others, too) have a custom trait "IsRelocatable" used in their own containers that describes whether a type is memcpy compatible:I assume the default template of IsRelocatable<T> derives from is_trivially_copyable<T>, that's at least what I would do to cover as many types as possible. Then fewer custom specializations are required. Though I guess in the end it would be nice to have such a trait in the standard for which user specializations are permitted and encouraged.
I disagree. Invert the suggestion: leave Is_trivially_copyable like it is
right now and add the new trait on top. All trivially copyable types can be
memcpy'ed, but the inverse is not true: some memcpy'able types aren't
trivially copyable.
> Then, to take Miro's suggestion, we would create some new
> user-specializable template "std::is_relocatable", where a user could tell
> explicitly whether a std::memcpy copy of an object of this type would
> preserve the representational value.
Wait, now you've lost me. Why do we need three traits?
It's not.
On Sun, Sep 14, 2014 at 8:40 PM, Matheus Izvekov <mizv...@gmail.com> wrote:
If user-defined functions/types could be used as attributes then you'd
basically just be adding a new more convenient way to use library type
traits, even. :) You'd have:
[[std::attr::is_relocatable]]
struct foo { ... };
rather than (not yet possible without namespace shenanigans):
struct foo { ... };
template<> struct std::is_relocatable<foo> : std::true_type {};
You'd need user-defined types/functions to be usable as attributes and
then a way to check if a type has an attribute. Then the library
implementation would be something vaguely like:
namespace attr { struct is_relocatable {}; }
template <typename T> struct is_relocatable :
std::integral_constant<bool, std::is_trivially_copyable_v<T> ||
std::has_attribute_v<T, attr::is_relocatable>> {};
This kind of attribute is already pretty similar to carries_dependency
I think: it doesn't change language semantics but rather just provides
hints to the implementation (in this case the library developer) that
some shortcuts can be taken that it can't figure out on its own.
Why can't a type with virtuals be memcpy'able? It depends on the ABI, of
course, but if the user knows the ABI allows for it, why not?
Still not.
Currently the standard doesn't allow this for is_trivially_copyable because it must have trivial copy/move constructors and that excludes classes with virtual member functions. This goes all the way back to the original issue that is_trivially_copyable is quite restrictive.
I think there are currently two problems open here:
1. A trait enforced by the compiler to tell you that memcpy'ing T would *definitely* cause UB. (This could be some subset of is_trivially_copyable.) This would also be the point where the compiler/ABI decide whether virtual member functions and other implementation-specific properties of the ABI disqualify for memcpy or not. This is not necessarily concerned about trivial copy/move ctors, dtors, operator= and so on.
2. A means for users to specify that a type supports memcpy even though it does not fulfill all requirements of is_trivially_copyable. However I think it should not be possible to override the safeties put in place by the compiler enforced trait in #1, e.g. if the ABI forbids memcpy'ing virtual classes it should not be possible to bypass this restriction.
3. A trait that is a combination of #1 and #2 that would ultimately be queried by users to determine whether they may call memcpy or not. If #2 for T does not exist the fallback is is_trivially_copyable. This trait is applied transitively for types which do not have a user provided specialization of #2.
Makes sense?
I think it's good to have some sort of terminology for this thread, so just for the sake of discussion let me name the traits from my list based on facebook's example:1. __is_memcpyable<T>: This trait is provided by the implementation. It is true if memcpy'ing T would not break the object from the implementation's point of view. It does not concern itself with pesky details like whether your copy constructor is trivial, has the proper semantics, does the right thing or whatever. This is purely technical. I put it with double underscores here because I don't think it requires to actually exist in the library and is for exposition only.2. supports_relocation<T>: This is a new trait that by default equals is_trivially_copyable<T>. Users are encouraged to specialize this to true_type where the requirements of is_trivially_copyable are too restrictive and they know what they're doing.3. is_relocatable<T>: This is a new trait that is actually queried by end users to determine whether they are allowed to memcpy T or not. It is defined as (is_trivially_copyable || (__is_memcpyable<T> && supports_relocation<T>)).This got me thinking whether it would be an option to go back and re-define is_trivially_copyable to pick up these specializations:
is_trivially_copyable :=/* new rules */(supports_relocation && __is_memcpyable) ||/* old rules */(is_trivially_{copy_constructible && move_constructible && copy_assignable && move_assignable && destructible})If this was accepted then supports_relocation could be renamed to is_relocatable to be more consistent with the other traits (although it would become the first is_* trait open for user specialization). The benefit of this approach is: code currently querying is_trivially_copyable would automatically pick up new types for which supports_relocation/is_relocatable gets specialized in the future.
However, I see no reason that an implementation should be disallowed from returning true for is_raw_memcpyable or whatever you want to call it for a class that has virtual functions or even virtual inheritance if the compiler knows for sure that its implementation of that class will have defined behavior when memcpy'd.
So I agree with the multiple-part definition:
1. A trait indicating whether a memcpy'ing a class will have defined behavior in terms of members, submembers, and implementation-specific internal information. That is, without regard to whether a class has constructors or destructors (except that virtual destructors could affect this on account of being virtual).
2. A trait indicating whether memcpy will have defined behavior based upon both #1 and whether the class has constructors/destructors. This is the current is_trivially_copyable. However, it'd be great if it could be forced true by the class. Whether the class can force it true even when #1 is false, I don't know. (This would mean that the programmer is saying, "compiler, I know how you think, and am willing to risk getting this wrong with the result blowing up in my face.")
3. A trait indicating that structure members go in the order specified, with base classes first, and with the first member (possibly of the first base class with a member) at offset 0, meaning reinterpret_cast from struct to first member is legal. Also between such structures with common initial sequences (sockaddr...). This is obviously just the unchanged is_standard_layout.
However, I would argue that there should be a 4th trait, based on the fact that it is formally probably possible:
4. A trait indicating that offsetof is valid and a constant-expression for a class. This is the OR of #1 and #3. The current Standard only permits #3 but #1 can be added with just a tweak to the rules. offsetof must be definable and a fixed value for the runtime of a program if it is possible to write memcpy within C++ itself (that is, that memcpy is not a "magic" function).
Requiring that offsetof be a constant-expression seems logical to me - the alternative would be to allow implementations to restructure classes at program start time. Since data member pointers are constant-expressions, this seems to match the concept.
Melissa
There is nothing in the Standard that says that virtual functions cannot be implemented in a way that involves storing a pointer to the base of the most-derived type of that instance (i.e. dynamic_cast<void *>(this)). Such an implementation would definitely blow up horribly / cause undefined behavior if memcpy'd. As much as I seem on the liberal side of things, I have to agree with this rule.However, I see no reason that an implementation should be disallowed from returning true for is_raw_memcpyable or whatever you want to call it for a class that has virtual functions or even virtual inheritance if the compiler knows for sure that its implementation of that class will have defined behavior when memcpy'd.
So I agree with the multiple-part definition:
2. A trait indicating whether memcpy will have defined behavior based upon both #1 and whether the class has constructors/destructors. This is the current is_trivially_copyable. However, it'd be great if it could be forced true by the class. Whether the class can force it true even when #1 is false, I don't know. (This would mean that the programmer is saying, "compiler, I know how you think, and am willing to risk getting this wrong with the result blowing up in my face.")
4. A trait indicating that offsetof is valid and a constant-expression for a class. This is the OR of #1 and #3. The current Standard only permits #3 but #1 can be added with just a tweak to the rules. offsetof must be definable and a fixed value for the runtime of a program if it is possible to write memcpy within C++ itself (that is, that memcpy is not a "magic" function).
Requiring that offsetof be a constant-expression seems logical to me - the alternative would be to allow implementations to restructure classes at program start time. Since data member pointers are constant-expressions, this seems to match the concept.
Melissa
Why?
If the class really IS trivially copyable, why do you want to add user
defined constructors and operators?
Clarifying that use case:
Yes. If this were MIPS, the default compiler-generated unaligned<int>::unaligned(const unaligned<int> &) would be an lw and sw pair of instructions to copy the base member, which would crash on MIPS due to being misaligned. unaligned does not know that it is misaligned - only structs foo and bar know this.
Microsoft created a third CV qualifier called __unaligned to resolve this, but they don't seem to fully implement it, even when it would matter. Visual C++ lets you qualify member functions as __unaligned just like they can be const or volatile. Grammatically, it works, but I've never seen __unaligned make Visual C++ actually do anything to the output code beyond affect overload selection.
Microsoft created a third CV qualifier called __unaligned to resolve this, but they don't seem to fully implement it, even when it would matter. Visual C++ lets you qualify member functions as __unaligned just like they can be const or volatile. Grammatically, it works, but I've never seen __unaligned make Visual C++ actually do anything to the output code beyond affect overload selection.
What architecture have you tried this on?
#include <cstdio>
#include <cstring>
#include <xmmintrin.h>
#define alignas(...) __declspec(align(__VA_ARGS__))
__declspec(noinline) void copy_sse_to_unaligned(__unaligned __m128 &dest, const __m128 &src)
{
dest = src;
}
int main(int argc, const char *const *)
{
alignas(16) unsigned char buffer[sizeof(__m128) * 2];
__m128 src;
__unaligned __m128 *dest = reinterpret_cast<__m128 *>(&buffer[1]);
std::memset(&src, 0x3F, sizeof(src));
copy_sse_to_unaligned(*dest, src);
std::printf("%g\n", dest->m128_f32[0]);
return 0;
}
x86-64. The example code below crashes. The __declspec(noinline) is necessary, because if it inlines, the optimizer actually somehow notices the misalignment and emits movups instead of movaps. This happens regardless of whether the pointer is __unaligned-qualified: adding #define __unaligned has no effect either way.
struct foo {
int bar;
foo() = default;
foo(const foo &) = delete;
foo(foo &&) = delete;
foo& operator=(const foo &) = delete;
foo& operator=(foo &&) = delete;
};
"trivially copyable" is a concept (defined term) in the core language section
of the standard.
There is indeed a corresponding trait in the library section, but we should
first inspect whether the core concept is sound before messing with
the library trait.
Thanks,
Jens
The concept "trivially copyable" is currently broken because it mixes two slightly related but different concepts:
1) That an object of a given type could be memcpy'ed without leading to undefined behaviour.
2) That a copy of an object made through memcpy would have the same representational value as it's original.
Isn't #2 a consequence of #1?
class foo {
int *p;
int a;
public:
foo() : p(&a) {}
foo(const foo &c) : p(&a), a(c.a) {}
int get() { return *p; }
void set(int val) { *p = a; }
};
Well, that type is not trivially copyable...
The code above is not trivially copyable and neither can it be memcpy'ed. I
don't see how that proves anything, since this example is entirely out of
scope of anything we're discussing.
My point is that, given a type T such that it is trivially copyable, then:
T a, b;
the two operations:
a = b;
and
memcpy(&a, &b, sizeof a);
Are the same. In other words, a type being trivially copyable is sufficient
condition for memcpy being allowed, producing the same representational value.
Note the "sufficient" part.
On 21 September 2014 23:29, Thiago Macieira <thi...@macieira.org> wrote:
> That, in specific, looks wrong to me. A class with a deleted copy constructor
> can't have a *trivial* copy constructor. It doesn't *have* a copy constructor,
> so we can't qualify it as trivial.
That's not correct. Deleted functions are defined, and they do participate in
overload resolution. Deleting a function does not mean the function is
suppressed.
I don't agree with the statement that it's broken. I think it is correct.
I think it's incomplete.
I said that "trivially copyable" is sufficient condition for memcpy. It's not a
necessary condition: there are plenty of classes that are memcpy'able but
aren't trivially copyable.
My point is that the compiler doesn't know and can't know. It's up to the
class author to declare it as such, with either an attribute or by way of a
specialisation.
That, in specific, looks wrong to me. A class with a deleted copy constructor
can't have a *trivial* copy constructor. It doesn't *have* a copy constructor,
so we can't qualify it as trivial.
That's not correct. Deleted functions are defined, and they do participate in
overload resolution. Deleting a function does not mean the function is
suppressed.
I don't know of a passage of the standard that says that. What I know is that
it is well-defined to memcpy a trivially-copyable object.
For any object (other than a base-class subobject) of trivially copyable type T, whether or not the object
holds a valid value of type T, the underlying bytes (1.7) making up the object can be copied into an array
of char or unsigned char.If the content of the array of char or unsigned char is copied back into the
object, the object shall subsequently hold its original value.
Standard layout types are compatible with C and, since the only way for them
to be copyable in C is via user-defined code, it stands to reason that all
standard layout types could be copied with memcpy if the members allow for it.
Therefore, as long as the user-defined copy constructor, move constructor, copy
assignment operator, move assignment operator and destructor allow for it, the
type could be copied by memcpy too.
Not with the current rules. Also, having a deleted copy constructor
doesn't necessarily
mean !is_trivially_copy_constructible. The traits specifically answer
the question
"is such-and-such expression valid?", not "does this class have
such-and-such special
member function".
On Sunday 21 September 2014 14:48:50 Matheus Izvekov wrote:
> So, putting all of this together, it means that a class with a deleted copy
> constructor has a copy constructor, but it is neither trivial nor
> non-trivial O_o
The empty set qualifies all properties.
The expression used for copying. And it's not valid or not depending on whether
the copy constructor is trivial. It all boils down to
is_trivially_constructible, which
says "is_constructible<T,Args...>::value is true and the variable definition for
is_constructible, as defined below, is known to call no operation that
is not trivial ( 3.9, 12)."
Check this out:
struct X {
X() = default;
X(const X&)=delete;
};
int main()
{
cout << is_trivially_copyable<X>::value;
cout << is_trivially_copy_constructible<X>::value;
}
clang/libc++ prints true and false. So, X is memcpy-able, but it
cannot be copy-constructed.
There have been suggestions to make X not trivially copyable if X has
deleted copy operations,
but such suggestions haven't been discussed in a meeting yet.
The passage you quoted does not say that.
The passage you quoted is the sufficient condition that all trivially copyable
types can be copied via trivial functions like memcpy.
It does not say anything about types that aren't trivially copyable.
I am saying that all standard layout types can be copied with user-defined
functions without triggering UB, since the whole point of standard layout is
that they are layout-compatible with C types (PODs). Whether the copied type
makes sense or not depends only on the user-defined copy/move constructors and
assignment operators, and on the destructor.
Where is the standard worded so?
A trivially copyable class is a class that:
— has no non-trivial copy constructors (12.8),
— has no non-trivial move constructors (12.8),
— has no non-trivial copy assignment operators (13.5.3, 12.8),
— has no non-trivial move assignment operators (13.5.3, 12.8), and
— has a trivial destructor (12.4).
I don't recall seeing a suggestion that a user could claim his class
to be trivially
copyable when it violates the language rules for trivially copyable types.
I don't see how that would be the intention, since nothing in the specification
cross-referenced there (12.4) says anything about being deleted.
What really strikes me as odd is that the example types in the first message in
this thread have user-provided copy constructors but you apparently don't care
whether they are invoked when objects of your class type are copied. I find it
very difficult to believe that's a case common enough to deserve core language
support.
template<class T> struct unaligned {
using type = T;
// static_assert(std::is_trivially_copyable<type>{}, "");
template<typename... Args> unaligned(Args&&... args) {
const type tmp(std::forward<Args>(args)...);
memcpy(&base, &tmp, sizeof(type));
}
unaligned(const unaligned &a) { *this = a; }
unaligned& operator =(const unaligned &a) {
memcpy(&base, &a.base, sizeof(type));
return *this;
}
operator type() const {
type ret;
memcpy(&ret, &base, sizeof(type));
return ret;
}
private:
type base;
};
Because there's only ever one destructor, and there may be multiple overloads
of the other special member functions, as I wrote in a follow-up.
> Is this something that would need an EWG issue to be resolved?
Probably.
Why don't you just leave out the actual copy operations and constrain
the templated one
that is not meant to be a copy so that it SFINAEs away when Args... is the same
type as the wrapper?
I must be missing some parts of the picture, now. If you can't use the
implicitly-defaulted
copy operations, how can your class ever be trivially copyable?
If you have a non-const unaligned, the template will be used for copying.
> But what do you mean by leaving out the actual copy operations?
> They can't be left out if I understand you right, the object is possibly
> unaligned, you can only touch it with a 10 foot pole (memcpy).
I must be missing some parts of the picture, now. If you can't use the
implicitly-defaulted
copy operations, how can your class ever be trivially copyable? Is the
intention so
that you can somehow detect when the object is properly aligned and
use the default
special member functions in that case, and not otherwise? It seems we are rather
far away from a situation where relaxing the constraints for trivial
copyability would
be a solution.
I’ve not been following so closely, but I recall an example wherein the class could *only* be copied by memcpy due to unalignment. The only safe copy-constructor is one that explicitly calls memcpy. The user wishes std::is_trivially_copyable to return true for the class. Of course, relying on the default copy operations would be unsafe because the implementation can (and probably will) generate code which relies on the actual alignment.
My gut instinct would be that a user-defined specialization of std::is_trivially_copyable should notify the standard library and any other introspection of what the user did. But this discussion is pretty far along and I’m missing a lot :P .
I must admit I don't follow. In your previous message you say that the
copy operations
can't be left out because you need to make sure they use memcpy and apparently
the implicitly-defaulted ones won't do. Now you're saying that they
supposedly have
the same semantics as the implicitly-defaulted ones. Which is it?
Yes, but that's as far as the superficial similarity of the semantics
goes. The semantics
are not the same, really.
That sounds like such cases should have a way to state that their user-provided
copy operations are almost-like-trivial, and UB is the result if
substituting them with
memcpy isn't right. In other words, these things are very different
from the existing
rules, in the sense that it's a request to make promises to users that
might not be
true, and hence such things would pass through compile-time checks but may
cause UB if the promises to such checks "lie".
The UB can, of course, be avoided if instead of allowing user-provided
copy operations
to be trivial we invent some syntax that says "default using memcpy".
Or probably
more generally, "default using bit-blasting that doesn't require
proper alignment".
The general idea is that attributes should not have a semantic effect,
and we're either
at or across the borderline of what is or is not a semantic effect
here, I think. I would
be very hesitant to make something like that an attribute.
struct foo {
foo(const foo &a) [[trivial]] {
// some core here that satisfies the semantics of
// the implicit-default copy constructor
}
};
Yes. :)
Well, perhaps not an attribute, but indeed have more stringent requirements
on what the implementation can do and what it must do, but to avoid saying
that it necessarily does memcpy specifically.
I guess the idea in general is worth further exploration, in whatever form.
[[unaligned]] alignas(2) int bar;
Well, clashes are less likely if you put such contextual keywords into suitable
locations. 'final' in class-head is an example of that, although it
did require small
adjustments to fit into the rest of the grammar.
struct foo {
foo(const foo &a) = trivial {
// some core here that satisfies the semantics of
// the implicit-default copy constructor
}
};
alignas(unaligned, 2) int bar;
struct alignas(unaligned) foo {
int bar; //same as alignas(unaligned) int bar;
};
struct alignas(unaligned) foo {
int bar; //same as alignas(unaligned) int bar;
};
alignas(-2) int bar;
struct alignas(-1) foo {
int bar; //same as alignas(-1) int bar;
};
struct alignas(-2) foo {
int bar; //same as alignas(-2) int bar;
};
struct alignas(-8) foo {
int bar; // no change here, in case alignof(int) < 8
};
I would certainly prefer a solution where that "some code" isn't written at all,
but rather either the class or just its copy operation is
syntactically marked to
do the right thing.
That's the difference between memcpy-copying and memcpy-moving.
The standard talks about memcpying away and then back. That would work for
this class above. The standard does not talk about memcpy-based relocation,
only about copying back to where it was.
For any trivially copyable type T, if two pointers to T point to distinct T objects obj1 and obj2, where
neither obj1 nor obj2 is a base-class subobject, if the underlying bytes (1.7) making up obj1 are copied
into obj2, 43 obj2 shall subsequently hold the same value as obj1. [Example:
T* t1p;
T* t2p;
// provided that t2p points to an initialized object ...
std::memcpy(t1p, t2p, sizeof(T));
// at this point, every subobject of trivially copyable type in *t1p contains
// the same value as the corresponding subobject in *t2p
On Monday 22 September 2014 14:03:41 Myriachan wrote:
> 1b. This requirement does not hold for subobjects; in particular, they can
> overlap noncontiguously with their virtual bases. However, the subobject
> rule is still subject to the definition of *memcpy-compatible*: a subobject
> that is *memcpy-compatible* would still have to be *memcpy-compatible*.
>
> 2. A *memcpy-compatible* class is one in which no class in the hierarchy
> has a virtual base or virtual functions.
And whose members are all of the same protection level. In other words, the
class is standard-layout.
An implementation is allowed to turn a type X that has multiple protection
levels to be (exercising your 1b above):
struct X_layout {
struct X_protecteds {
/* protected members go here */
} *protecteds;
struct X_private {
/* private members go here */
} *privates;
/* public members go here */
};
sizeof(X) is the sum of all three structs and the compiler manages the two
hidden pointers. That way, when X is the most derived type, it still occupies
a contiguous array of bytes, but it's not relocatable. What's worse, a Y
deriving from X could make X's sub-object non-contiguous.
> 2b. std::memcpy on a *memcpy-compatible* but non-*trivially-copyable* class
> is well-defined, but may still end up resulting in undefined behavior if
> the fact that constructors and destructors are not invoked is an issue. (
> std::string is *memcpy-compatible* by this definition, and is possibly even
> *standard-layout*, but copying one with std::memcpy would be a really bad
> idea.)
That's the difference between memcpy-copying and memcpy-moving.
The standard talks about memcpying away and then back. That would work for
this class above. The standard does not talk about memcpy-based relocation,
only about copying back to where it was.
> 2e. The order of class members and of base class subobjects in memory have
> no additional constraints versus C++14: offsetof is really only useful for
> finding a single element when used on a class that is not *standard-layout*.
I disagree. The requirement should be that the class is standard-layout in the
first place. Class X above could not use offsetof for any private or protected
member.
Alternatively, we add a new trait that indicates whether a type is
offsetof'able and memcpy'able, and at least all standard layout types must be
included in this new trait, but an implementation may include others too.
> 2f. The quality of being *memcpy-compatible* is represented in the type
> trait std::is_memcpy_compatible.
> 2g. An implementation may be able to safely memcpy classes with virtual
> inheritance, virtual functions, or both, or may be able to under certain
> conditions; the implementation can indicate this by making
> std::is_memcpy_compatible<C>::value be true for class C.
Such as this trait.
> 2i. As an exception, memcpy on an empty base class subobject that has been
> elided to size zero is undefined. (Perhaps we should have a trait for
> detecting this case?)
Suggestion: std::is_empty_aggregate.
MostDerived *derived = ???;
Base *base = static_cast<MostDerived::IntermediateBase2::IntermediateBase1::...::Base *>(derived);
std::is_empty_aggregate<Base, IntermediateBase1, IntermediateBase2, MostDerived>::value
> 3. A *trivially-copyable* class is a *memcpy-compatible* class in which no
> class in the hierarchy has a nontrivial copy constructor, nontrivial move
> constructor, nontrivial copy assignment operator or nontrivial move
> assignment operator, and in which every class has a trivial destructor.
> 3a. *TODO:* We need some way to set this flag on for *memcpy-compatible*
> classes that do not meet these criteria.
Allow the user overriding std::is_memcpy_compatible. And please split the
trait in two: memcpy copying and memcpy moving.
> 5. One more rule I think would be worth changing relates to the fixed
> layout you get with *memcpy-compatible* classes. If a *memcpy-compatible*
> class derives from exactly one nonempty base class and any number of empty
> base classes (that don't trip the unique address rule), has no non-static
> data members of its own, and defines no virtual functions other than ones
> that use or could use the override contextual keyword, then if you have an
> array of objects of the derived class, it's safe to use pointer arithmetic
> with a pointer to the nonempty base class.
This is ABI-dependent because you included virtuals. I'd rather not go there.
Case in point: you did not exclude virtuals from those other, empty bases.
Therefore, they are not actually empty, since they must have at least the
vtable pointer(s) present. In the portable IA-64 C++ ABI, overriding a virtual
from a non-primary base is equivalent to adding a new virtual. Covariant
overrides are also the same as new virtuals.
So no, don't go there.
I think you're putting the cart ahead of the horse.
We already have the class of types that are contiguous: standard layout. We
just need a way to restrict the group of all standard layout types to all
those that can be memcpy-relocated and those that can be memcpy-cloned.
> > I disagree. The requirement should be that the class is standard-layout in
> > the
> > first place. Class X above could not use offsetof for any private or
> > protected
> > member.
>
> Definitely true if your class X is allowed to be implemented as you showed.
So it just goes to show that the ability to use offsetof is the same as
standard layout.
I don't see why we need the path. If std::is_empty_aggregate<Base>::value is
true, you should never attempt to copy it, whether it be sub-object of a
larger object or full object. If it's a full object, it contains one byte of
unspecified value that mustn't be read from or written to.
Why do we need a definition for memcpy-compatible-layout to be different from
that of standard layout in the first place? Are we trying to capture the case
of non-empty deriving from non-empty? Those aren't standard layout but under
most ABIs they could be memcpy'ed whether they are sub-objects or full
objects. In fact, they are trivially copyable.
If that's the use-case you're aiming for, I have to ask first why standard
layout was restricted to only one non-empty class in the hierarchy. There must
have been a reason why C++11 gave it to us like that and that might be a
limitation on the ability to memcpy a full object.
BTW, I think we have now four traits to specify:
- s_empty_aggregate (because sizeof(X) can't be trusted)
- is_contiguous (something like your definition above, with better name)
those two are mutually exclusive
- is_memcpy_relocatable
- is_memcpy_clonable
The latter two are true for all trivially copyable types and false for others,
but may be overridden by the user to indicate that their type is relocatable
or clonable.