2

For std::atomic the copy constructor is deleted, and this should only compile with C++17 and higher due to copy elision:

std::atomic<int> t_int = 1; 

I expected that it does not compile using -fno-elide-constructors flag, but it still compiles:

https://godbolt.org/z/nMvG5vTrK

Why is this?

Ari
  • 7,251
  • 11
  • 40
  • 70
  • 2
    Why do you expect the copy constructor to be used here at all? That's an initialization, and it uses the [converting constructor](https://en.cppreference.com/w/cpp/atomic/atomic/atomic) (overload 2) – Nathan Pierson May 19 '22 at 16:16
  • 1
    `fno-elide-constructors` doesn't work in C++17. There is nothing to be elided by the compiler because the language says that `std::atomic t_int = 1;` is actually `std::atomic t_int{1}; `. You can see that if you go back to C++14, then it will fail as you expect: https://godbolt.org/z/aWo94EPKo – NathanOliver May 19 '22 at 16:20
  • @NathanOliver: Where does the language say this and why did this behavior change in C++17? I understand that it changed in C++17. But I was hoping I can find a reference to it, i.e. what exactly is this behavior called?. – Ari May 19 '22 at 16:21
  • 1
    @Ari C++17 was shipped with a feature called guaranteed copy elision. That feature changed what a temporary is and how/when it is materialized. One of those changes is that the optimization that compilers did by eliding the temporary in copy initialization away became a standard part of the language and no longer just a compiler optimization. – NathanOliver May 19 '22 at 16:24
  • `std::atomic t_int = 1; ` is initialisation using constructor (2) see https://en.cppreference.com/w/cpp/atomic/atomic/atomic . There is __no__ copying involved. – Richard Critten May 19 '22 at 16:25
  • 3
    @RichardCritten That's only true since C++17. Before then `1` would need to be converted to a `std::atomic` and then `t_int` is initialized from that temporary object since using `=` means we are doing copy initialization. You can see this is the case as the code fails to compile in C++14 which does not have guaranteed copy elision. – NathanOliver May 19 '22 at 16:27
  • @NathanOliver: right, based on your description before C++17, the copy constructor should have been called. I expected `-fno-elide-constructors` to disable `guaranteed copy elision`, but looks like I'm missing something. – Ari May 19 '22 at 16:29
  • @NathanOliver Constructor (2) is (since C++11) and when T is `int` is directly used as it's not `explicit` – Richard Critten May 19 '22 at 16:30
  • @RichardCritten If that was true, why doesn't [this](https://godbolt.org/z/MnYrfacfK) compile? – NathanOliver May 19 '22 at 16:31
  • @NathanOliver and `std::atomic t_int { 1 };` does compile - live - https://godbolt.org/z/4WPaa5951 - now confused. – Richard Critten May 19 '22 at 16:34
  • 1
    @RichardCritten Nevertheless, the `= 1;` syntax is [copy initialization](https://en.cppreference.com/w/cpp/language/copy_initialization) which prior to C++17 did require a (potentially-elided) call to the copy constructor. – Nathan Pierson May 19 '22 at 16:34
  • 1
    @RichardCritten It's because when using `=`, you are in a copy initialization context. Before C++17, that meant you needed to convert the RHS to be the same type as the LHS so you can make a copy. This means making a temporary and then doing a copy of said temporary. Since C++17, that's no longer the case and the prvalue is elided away and we are just doing direct initialization. – NathanOliver May 19 '22 at 16:36
  • @NathanPierson Before C++17, using `converting constructor`, only the right hand side is constructed. The copy constructor should still be called to copy the right hand side into the left hand side. – Ari May 19 '22 at 16:48

2 Answers2

6

C++17 doesn't simply say that the previously optional return value optimizations are now mandatory. The actual description of the language changed so that there is no creation of a temporary object anymore in the first place.

So, since C++17, there is no constructor call that could be elided anymore. Hence it makes sense that -fno-elide-constructors doesn't add any temporary creation. That would be against the language rules.

Before C++17 the language describes that a temporary object is created from which the variable is initialized and then adds that a compiler is allowed to elide this temporary. Therefore, whether -fno-elide-constructors is used or not, the compiler is behaving standard compliant by eliding or not eliding the temporary copy.

user17732522
  • 53,019
  • 2
  • 56
  • 105
  • There is no copying in `std::atomic t_int = 1; ` it directly uses constructor (2) https://en.cppreference.com/w/cpp/atomic/atomic/atomic as this constructor is not `explicit`. – Richard Critten May 19 '22 at 16:29
  • 2
    @RichardCritten Before C++17, copy-initialization always creates a copy. Give me a sec for the standard reference. – user17732522 May 19 '22 at 16:30
  • @RichardCritten See https://timsong-cpp.github.io/cppwp/n4140/dcl.init#17.6.2: "_The result of the call (which is the temporary for the constructor case) is then used to direct-initialize, according to the rules above, the object that is the destination of the copy-initialization._", see also previous sentences. – user17732522 May 19 '22 at 16:33
  • @user17732522 Ah this explains why `-fno-elide-constructors` doesn't work here, but then does it mean `-fno-elide-constructors` is of no use in C++17? – Ari May 19 '22 at 16:35
  • @Ari In C++17 (and later) there are still some non-mandatory cases where copies can be elided, e.g. named return value optimization. The flag will presumably still effect those cases. – user17732522 May 19 '22 at 16:36
2

I expected that it does not compile using -fno-elide-constructors flag,

The given flag has no effect on return value optimization(aka RVO) from C++17 and onwards. This is because it is not considered an optimization anymore(from C++17) but instead it is a language guarantee.

From mandatory copy elison:

Under the following circumstances, the compilers are required to omit the copy and move construction of class objects, even if the copy/move constructor and the destructor have observable side-effects. The objects are constructed directly into the storage where they would otherwise be copied/moved to. The copy/move constructors need not be present or accessible:

  • In the initialization of an object, when the initializer expression is a prvalue of the same class type (ignoring cv-qualification) as the variable type:

(emphasis mine)

This means that t_int is constructed directly from the prvalue 1. And the flag -fno-elide-constructors has no effect on RVO which is different from NRVO. And so the copy constructor being deleted has no effect in your case.

Perhaps an example might help illustrating the same,

struct Custom 
{
    public:
      Custom(int p): m_p(p)
      {
          std::cout<<"converting ctor called"<<std::endl;
      }
      Custom(const Custom&) = delete; //deleted copy ctor
    private:
      int m_p = 0;
};
int main()
{
    Custom c = 1; //works in C++17 but not in Pre-C++17
}
Jason
  • 36,170
  • 5
  • 26
  • 60