Move constructor not inherited nor default generated
Asked Answered
M

2

7

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?
Mixedup answered 2/5, 2020 at 17:58 Comment(6)
One approach is to simply write a free function that takes std::ifstream& as one of its arguments. Inheritance seems like overkill here.Sardine
If it was as simple as implementing the new function, it wouldn't be an overkill.Mixedup
Strangely, this makes it compile: BinFile(BinFile&& other) : std::ifstream(std::move(other)) {} Not sure how this is different from ` = default;`Crooks
Your code works as is for me.Sisterinlaw
@0x499602D2 Sorry, it works probably because of something related to C++ 17 copy elision. My real code is a little different, I've update to the version that does not work in C++17.Mixedup
Usually it is not recommended to derive for a standard class. Here, it is much more simpler to write a free function and a lot better design as it could works with all input streams class and not only file input stream. Also, you should avoid reading and writing binary data. The above code is really fragile as it is sensible to data layout (size, byte order, padding…) and this can vary by OS, compiler. It make the code not portable and also make versioning hard. This is why most software store data in XML or JSON format.Canella
I
4

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.

Impeccant answered 2/5, 2020 at 21:20 Comment(5)
It’s also worth pointing out that you can’t really inherit a move constructor (they get discarded by overload resolution), and that that wouldn’t avoid the virtual base initialization anyway.Hildredhildreth
@DavisHerring I don't understand what you are talking about. If I replace ifstream with unique_ptr<int>, the move constructor works.Mixedup
In this case, where I explicitly said using std::ifstream::ifstream;, it should be obvious how to construct the virtual base class, no? I mean, it seems that C++11, who introduced this syntax, simply "forgot" this corner case.Mixedup
@lvella: Without virtual inheritance, the defaulted move constructor for the derived class invokes the one required, corresponding base class constructor, so it looks “inherited”. As for the syntax, it’s supposed to cover cases where the class inheriting the constructors is directly derived from multiple bases.Hildredhildreth
@Ivella using std::ifstream::ifstream; doesn't provide a copy/move constructor, it just lets you construct the derived class from a glvalue of the base class as well as from its other constructor parameter lists. A copy constructor still needs to be synthesized, and it would be odd for it to hand responsibility for initializing virtual bases to the immediate base class, even with inheriting constructors - if two bases' constructors were inherited in diamond inheritance, which should get responsibility for initializing the shared virtual base?Impeccant
F
0

As the previous answer mentioned the constructor is defined as deleted in the base class. It means, you can't use it - see <istream>:

__CLR_OR_THIS_CALL basic_istream(const basic_istream&) = delete;
basic_istream& __CLR_OR_THIS_CALL operator=(const basic_istream&) = delete;

And the return ret tries to use the deleted copy constructor and not the move constructor.

However if you crate your own move constructor it should work:

BinFile(BinFile&& other) : std::ifstream(std::move(other))
{

}

You can see this in one of the comments of your question (@igor-tandetnik).

Fearless answered 2/5, 2020 at 22:10 Comment(0)

© 2022 - 2024 — McMap. All rights reserved.