I don't disagree with Ted Lyngmo's answer, but I think this may provide more context:
A type is trivial if:
- it has a trivial default constructor; [*]
- every eligible copy constructor, move constructor, copy-assignment operator, or move-assignment operator it has is trivial, and moreover it has at least one of these; and
- its destructor is trivial.
Your type is nontrivial because it doesn't satisfy condition (2). It has one eligible copy constructor that's trivial, and one that isn't.
To me, this just pushes the question back a step (although maybe to a real expert all the following is obvious): why can't we write
struct T {
int a;
T() = default;
T(const T &) = default;
T(const volatile T &src) = default;
};
If this were allowed, presumably the generated copy constructor for a const volatile reference would match what you've written here, and then T
would satisfy the definition of a trivial type.
The problem with this is that it would mean that any time you write a flat struct it would automatically come equipped with a copy constructor taking a volatile instance, and that could be dangerous. Suppose we have a struct like
struct X
{
char data[256];
int num_reads;
int num_writes;
};
and we have a volatile X
instance mapped to memory in such a way that every time we read some data[i]
it causes num_reads
to increment. This is a perfectly legitimate situation when we're using volatile
, and a situation in which we obviously don't want the compiler generating a default copy constructor.
In the other direction, I guess we could ask why the constructor taking a const volatile reference is even considered a copy constructor at all. If you do provide such a constructor, it can be used to copy a non-volatile instance (assuming you didn't also provide another constructor for that case), which is maybe a point in favor of calling it a copy constructor. On the other hand, I guess it wouldn't be that hard to write something like
S(const S& s) : S(static_cast<const volatile S&>(s)) { }
to forward to it. (But there's a fair chance I'm missing some good reason that we need to regard this as a copy constructor to make something work properly -- maybe to let generic collection types take a volatile type parameter?)
That said, if this is causing you a problem, and you don't need to constructor taking a const volatile reference to count as a copy constructor, it would be easy enough to write this:
struct T {
int a;
T() = default;
T(const T &) = default;
T(const volatile T &src, bool) { a = src.a; }
};
Unfortunately you can't go a step further and provide a default argument for that dummy parameter, because that makes it a copy constructor again! But here's a version that does let you get away with a little more:
struct T {
int a;
T() = default;
T(const T &) = default;
template<int = 0> T(const volatile T &src) { a = src.a; }
};
This works because template constructors do not count as copy constructors. I'm not sure of the exact rationale there, but my assumption is that in general it's probably equivalent to the halting problem to determine if a template constructor has some set of arguments that give it the same signature as a copy constructor. Assuming that's the case, it would make it uncomputable in general to determine whether a type was trivially copyable or not, which would kind of blunt the usefulness of the concept.
[*] Technically, the condition is actually that every eligible default constructor is trivial, and moreover there's at least one of these. What's the difference? A type can have more than one default constructor:
struct S
{
S() = default;
S(int x = 0) { }
};