Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

Move constructor not inherited nor default generated

I tried extending std::ifstream with one function to make it easier to read binary variables, and to my surprise, with using std::ifstream::ifstream; the move constructor is not inherited. Worse yet, it is explicitly deleted.

#include <fstream>

class BinFile: public std::ifstream
{
public:
    using std::ifstream::ifstream;
    //BinFile(BinFile&&) = default; // <- compilation warning: Explicitly defaulted move constructor is implicitly deleted

    template<typename T>
    bool read_binary(T* var, std::streamsize nmemb = 1)
    {
        const std::streamsize count = nmemb * sizeof *var;
        read(reinterpret_cast<char*>(var), count);
        return gcount() == count;
    }
};

auto f()
{
    std::ifstream ret("some file"); // Works!
    //BinFile ret("some file"); // <- compilation error: Call to implicitly-deleted copy constructor of 'BinFile'
    return ret;
}

I don't want to explicitly implement the move constructor because it just feels wrong. Questions:

  1. Why it is deleted?
  2. Does it makes sense for it to be deleted?
  3. Is there a way to fix my class so that the move constructor is properly inherited?
like image 584
lvella Avatar asked May 02 '20 17:05

lvella


People also ask

Does compiler provide default move constructor?

In C++, the compiler creates a default constructor if we don't define our own constructor. In C++, compiler created default constructor has an empty body, i.e., it doesn't assign default values to data members. However, in Java default constructors assign default values.

Is a constructor not inherited?

Constructors are not members, so they are not inherited by subclasses, but the constructor of the superclass can be invoked from the subclass.

Should move constructor be Noexcept?

Move constructors of all the types used with STL containers, for example, need to be declared noexcept . Otherwise STL will choose copy constructors instead. The same is valid for move assignment operations.

Can move constructor throw?

Yes, throwing move constructors exist in the wild. Consider std::pair<T, U> where T is noexcept-movable, and U is only copyable (assume that copies can throw). Then you have a useful std::pair<T, U> move constructor which may throw.


1 Answers

The issue is that basic_istream (a base of basic_ifstream, of which template ifstream is an instantiation) virtually inherits from basic_ios, and basic_ios has a deleted move constructor (in addition to a protected default constructor).

(The reason for virtual inheritance is that there is a diamond in the inheritance tree of fstream, which inherits from ifstream and ofstream.)

It's a little known and/or easily forgotten fact that the most derived class constructor calls its (inherited) virtual base constructors directly, and if it does not do so explicitly in the base-or-member-init-list then the virtual base's default constructor will be called. However (and this is even more obscure), for a copy/move constructor implicitly defined or declared as defaulted, the virtual base class constructor selected is not the default constructor but is the corresponding copy/move constructor; if this is deleted or inaccessible the most derived class copy/move constructor will be defined as deleted.

Here's an example (that works as far back as C++98):

struct B { B(); B(int); private: B(B const&); };
struct C : virtual B { C(C const&) : B(42) {} };
struct D : C {
    // D(D const& d) : C(d) {}
};
D f(D const& d) { return d; } // fails

(Here B corresponds to basic_ios, C to ifstream and D to your BinFile; basic_istream is unnecessary for the demonstration.)

If the hand-rolled copy constructor of D is uncommented, the program will compile but it will call B::B(), not B::B(int). This is one reason why it is a bad idea to inherit from classes that have not explicitly given you permission to do so; you may not be calling the same virtual base constructor that would be called by the constructor of the class you are inheriting from if that constructor were called as a most-derived class constructor.

As to what you can do, I believe that a hand-written move constructor should work, since in both libstdc++ and libcxx the move constructor of basic_ifstream does not call a non-default constructor of basic_ios (there is one, from a basic_streambuf pointer), but instead initializes it in the constructor body (it looks like this is what [ifstream.cons]/4 is saying). It would be worth reading Extending the C++ Standard Library by inheritance? for other potential gotchas.

like image 131
ecatmur Avatar answered Sep 29 '22 11:09

ecatmur