1

In the following code the move constructor of the derived class is obviously not generated although the base class is move constructible.

#include <cstddef>
#include <memory>
#include <cstring>
#include <cassert>

template <typename T>
class unique_array : public std::unique_ptr<T[],void (*)(void*)>
{   size_t Size;
 protected:
    typedef std::unique_ptr<T[],void (*)(void*)> base;
    unique_array(T* ptr, size_t size, void (*deleter)(void*)) noexcept : base(ptr, deleter), Size(size) {}
 public:
    constexpr unique_array() noexcept : base(NULL, operator delete[]), Size(0) {}
    explicit unique_array(size_t size) : base(new T[size], operator delete[]), Size(size) {}
    unique_array(unique_array<T>&& r) : base(move(r)), Size(r.Size) { r.Size = 0; }
    void reset(size_t size = 0) { base::reset(size ? new T[size] : NULL); Size = size; }
    void swap(unique_array<T>&& other) noexcept { base::swap(other); std::swap(Size, other.Size); }
    size_t size() const noexcept { return Size; }
    T* begin() const noexcept { return base::get(); }
    T* end() const noexcept { return begin() + Size; }
    T& operator[](size_t i) const { assert(i < Size); return base::operator[](i); }
    unique_array<T> slice(size_t start, size_t count) const noexcept
    {   assert(start + count <= Size); return unique_array<T>(begin() + start, count, [](void*){}); }
};

template <typename T>
class unique_num_array : public unique_array<T>
{   static_assert(std::is_arithmetic<T>::value, "T must be arithmetic");
 public:
    using unique_array<T>::unique_array;
    unique_num_array(unique_num_array<T>&& r) : unique_array<T>(move(r)) {}
    unique_num_array<T> slice(size_t start, size_t count) const noexcept
    {   assert(start + count <= this->size()); return unique_num_array<T>(this->begin() + start, count, [](void*){}); }
 public: // math operations
    void clear() const { std::memset(this->begin(), 0, this->size() * sizeof(T)); }
    const unique_num_array<T>& operator =(const unique_num_array<T>& r) const { assert(this->size() == r.size()); memcpy(this->begin(), r.begin(), this->size() * sizeof(T)); return *this; }
    const unique_num_array<T>& operator +=(const unique_num_array<T>& r) const;
    // ...
};

int main()
{   // works
    unique_array<int> array1(7);
    unique_array<int> part1 = array1.slice(1,3);
    // does not work
    unique_num_array<int> array2(7);
    unique_num_array<int> part2 = array2.slice(1,3);
    // test for default constructor
    unique_num_array<int> array3;
    return 0;
}

With the above code I get an error (gcc 4.8.4):

test6.cpp: In function ‘int main()’: test6.cpp:47:48: error: use of deleted function ‘unique_num_array::unique_num_array(const unique_num_array&)’ unique_num_array part2 = array2.slice(1,3);

The slice function in the derived class cannot return by value because the move constructor is missing. All other constructors seem to work as expected (not covered by this example).

If I define the move constructor explicitly (uncomment line) the example compiles. But in this case the default constructor vanishes which is, of course, not intended.

What is going on here? I do not understand either of the cases.

Why is the move constructor deleted in the first case?

Why is the default constructor dropped in the second case? Others seem to survive.

Marcel
  • 1,688
  • 1
  • 14
  • 25
  • 1
    [Compiles with g++ 5.4.0](http://rextester.com/MXN10456), for what it's worth. – Igor Tandetnik Dec 25 '17 at 13:00
  • You should prefer composition over inheritance. – Jarod42 Dec 25 '17 at 13:52
  • You might consider using `std::vector`. – Jarod42 Dec 25 '17 at 13:54
  • @Jarod42: I am refactoring old code with some resource leaks and other not that pretty things. In fact there is another class inheriting from unique_num_array that allocates special memory for fast number crunching libraries. std::vector could do that with a custom allocator, but it has the disadvantage that it is resizable and copyable. Both is not intended in this case. And AFAIK the standard still does not provide a container with immutable size. – Marcel Dec 26 '17 at 07:14

2 Answers2

4

Why is the move constructor deleted in the first case?

Because there is a user-declared copy assignment operator in unique_num_array<T>, no move constructor is implicitly declared by the compiler. The standard in [class.copy.ctor]/8 says

If the definition of a class X does not explicitly declare a move constructor, a non-explicit one will be implicitly declared as defaulted if and only if

  • X does not have a user-declared copy constructor,

  • X does not have a user-declared copy assignment operator,

  • X does not have a user-declared move assignment operator, and

  • X does not have a user-declared destructor.


Why is the default constructor dropped in the second case?

Because there is a user-declared move constructor in unique_num_array<T>, no default constructor is implicitly declared by the compiler. The standard in [class.ctor]/4 says

... If there is no user-declared constructor for class X, a non-explicit constructor having no parameters is implicitly declared as defaulted ([dcl.fct.def]).


In addition, this code will work after C++17 because of guaranteed copy elision. In detail, before C++17, the semantic of both the context

return unique_num_array<T>(...);

and

unique_num_array<int> part2 = array2.slice(1,3);

requires a copy/move operation, while after C++17, the semantic becomes that the destination object is initialized by the prvalue initializer without ever materializing a temporary, thus no copy/move is required.

Community
  • 1
  • 1
xskxzr
  • 12,442
  • 12
  • 37
  • 77
  • There was one important part missing, but your answer definitely put me into the right direction. The using directive does not handle all constructors (as I expected). – Marcel Dec 26 '17 at 07:34
1

There are two sets of rules that apply here:

  1. Neither the move constructor nor the default constructor is covered by the using directive.

    [...] All candidate inherited constructors that aren't the default constructor or the copy/move constructor and whose signatures do not match user-defined constructors in the derived class, are implicitly declared in the derived class.

  2. Now the rules for auto generating non-explicit constructors apply (as xskxsr already mentioned).

    If the definition of a class X does not explicitly declare a move constructor, a non-explicit one will be implicitly declared as defaulted if and only if [...] X does not have a user-declared copy assignment operator

    [...] If there is no user-declared constructor for class X, a non-explicit constructor having no parameters is implicitly declared as defaulted ([dcl.fct.def]).

Marcel
  • 1,688
  • 1
  • 14
  • 25