Home > Enterprise >  Why non-volatile T with conversion from volatile T to T should be non-trivial?
Why non-volatile T with conversion from volatile T to T should be non-trivial?

Time:10-13

    struct T {
        int a;
        T() = default;
        T(const T &) = default;
        T(const volatile T &src) { a = src.a; } 
    };

If we don't provide the copy ctor which takes a cvref parameter, then T is trivial, not otherwise.

By C standard, looks like it's expected.

I can understand volatile T is not trivial, because a volatile type may need different copying method.

But, I don't quite understand why a non-volatile type T which has an user-provided conversion from volatile T to T should be considered as non-trivial as well?

Any comment would be appreciated.

CodePudding user response:

I don't quite understand why a non-volatile type T which has an user-provided conversion from volatile T to T should be considered as non-trivial`

It's not that it's taking a const volatile T& that matters here. It's that's it's user-provided.

This is not trivial either:

struct T {
    int a;
    T() = default;
    T(const T & src) : a{src.a} {}   // note: a user-provided copy constructor
};

For a type to be considered trivial, it can't have a non-trivial copy constructor. A non-trivial copy constructor is one that is neither implicitly-defined nor defaulted.

Your copy constructor that takes a const volatile T& is neither implicitly-defined nor defaulted, hence it's not trivial.

CodePudding user response:

I don't disagree with Ted Lyngmo's answer, but I think this may provide more context:

A type is trivial if:

  1. it has a trivial default constructor; [*]
  2. 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
  3. 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; }
};

[*] 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) { }
};
  • Related