8

Assume the following c++17 code:

#include <type_traits>
namespace dtl
{
   struct One
   {
      explicit One(int);
      ~One() = default;
      One(const One &) = delete;
      auto operator=(const One &) -> One & = delete;
      auto operator=(One &&) -> One & = delete;
      One(One &&); // Throwable, not default;
      int m_int;
   };
   struct Two 
   {
      explicit Two(int);
      ~Two() = default;
      Two(const Two &) = delete;
      auto operator=(const Two &) -> Two & = delete;
      auto operator=(Two &&) noexcept -> Two & = delete;
      Two(Two &&) noexcept = default;
      One m_one;
   };
   One::One(One &&) { throw 1; }

   static_assert(std::is_nothrow_move_constructible_v<Two>);
}

Code at compiler-explorer

Here we clearly see that the move constructor of the class One ain't marked as noexcept. Class Two has a defaulted move constructor that is explicitly requested to be noexcept.

If we check this code, this compiles with GCC trunk, Clang trunk, MSVC 19.28 and fails with MSVC19.24.

I checked the following sources which seem to tell me that the move constructor of Two needs to be deleted:

CWG issue 1778 to read (N4296 [dcl.fct.def.default]/p3):

If a function that is explicitly defaulted is declared with an exception-specification that is not compatible (15.4) with the exception specification on the implicit declaration, then

if the function is explicitly defaulted on its first declaration, it is defined as deleted; otherwise, the program is ill-formed.

Based on that information, I can only conclude that all 3 compilers are wrong in considering Two as no_throw_move_constructible and the move constructor should be implicitly deleted. As it's strange for all 3 to ignore the standard on this, I do wonder: Is this really a compiler bug or am I missing something.

Shayan Shafiq
  • 1,447
  • 5
  • 18
  • 25
JVApen
  • 11,008
  • 5
  • 31
  • 67

1 Answers1

5

I believe that you are looking at outdated information. DR1778 has been superceded by P1286R2. If you look at the implementation status, you will see that gcc 10 and clang 9 implement this new resolution.

Indeed, if you go back to older gcc versions in godbolt, it tells you:

<source>: In function 'int main()':
<source>:35:25: error: use of deleted function 'dtl::Two::Two(dtl::Two&&)'
   35 |     auto b = std::move(a);
      |                         ^
<source>:23:7: note: 'dtl::Two::Two(dtl::Two&&) noexcept' is implicitly deleted because its exception-specification does not match the implicit exception-specification ''
   23 |       Two(Two &&) noexcept = default;
      |       ^~~
Compiler returned: 1

You can find the gcc discussion here. According to this list, P1286R2 was accepted as a DR, meaning that it was retroactively applied to previous standards. As such, newer compilers will behave in the way that you noticed, independent of the chosen C++ standard.

At runtime, however, this will fail as expected:

dtl::One::One(int) {};
dtl::Two::Two(int) : m_one(0) {};

int main() {
   auto a = dtl::Two{1};
   try {
      auto b = std::move(a);
   } catch (...) {
      // Even though an exception is thrown, it will not be caught here because
      // we broke our `noexcept` promise.
      std::cout << "caught" << std::endl;
   }
   return 0;
}
[:~/tmp] $ /usr/local/Cellar/llvm/11.0.0/bin/clang++ -std=c++17 mv.cpp  && ./a.out
libc++abi.dylib: terminating with uncaught exception of type int
Abort trap: 6
mrks
  • 8,033
  • 1
  • 33
  • 62
  • Thanks for the input, so am I correct to say that `Two(Two &&) noexcept = default;` behaves as if we would have written `Two(Two &&) noexcept = default;` and will call `std::terminate` if an exception is thrown? – JVApen Dec 31 '20 at 14:44
  • You wrote `Two(Two &&) noexcept = default;` twice, but yes, `terminate` will be called. I updated my answer accordingly. – mrks Dec 31 '20 at 15:08
  • Oeps, copy-paste error, I meant `noexcept {}` iso `noexcept = default; `. Sounds good, thanks – JVApen Dec 31 '20 at 15:19