移动构造函数不会被继承也不会默认生成

7

我尝试通过扩展std::ifstream的一个函数来更轻松地读取二进制变量,结果让我惊讶的是,使用using std::ifstream::ifstream;并未继承移动构造函数。更糟糕的是,它被明确地删除了。

#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;
}

我不想显式实现移动构造函数,因为这样感觉不对。问题:

  1. 为什么它被删除了?
  2. 它被删除有意义吗?
  3. 有没有一种方法可以修复我的类,使移动构造函数得到正确继承?

1
一种方法是简单地编写一个自由函数,其中一个参数是std::ifstream&。继承在这里似乎有点过头了。 - Pete Becker
如果只是实现新功能这么简单,那就不会过度了。 - lvella
1
奇怪的是,这样就可以编译通过: BinFile(BinFile&& other) : std::ifstream(std::move(other)) {} 不确定它和 = default; 有何区别。 - Igor Tandetnik
你的代码对我来说 就是这样 能够正常工作。 - David G
@0x499602D2 抱歉,这可能是由于与 C++17 复制省略相关的问题。我的真实代码有些不同,我已经更新到在 C++17 中无法工作的版本。 - lvella
通常不建议从标准类派生。在这里,编写一个自由函数会更简单,而且设计更好,因为它可以与所有输入流类一起使用,而不仅仅是文件输入流。此外,应避免读取和写入二进制数据。上面的代码非常脆弱,因为它对数据布局(大小、字节顺序、填充等)敏感,而这些可能因操作系统、编译器而异。这使得代码不可移植,也使版本控制变得困难。这就是为什么大多数软件将数据存储在XML或JSON格式中的原因。 - Phil1970
2个回答

4
问题在于basic_istreambasic_ifstream的基类,其中模板ifstream是一个实例化)虚拟地继承自basic_ios,而basic_ios有一个删除的移动构造函数(除了受保护的默认构造函数)。
(使用虚拟继承的原因是,fstream的继承树中存在一个菱形,该菱形继承自ifstreamofstream。)
鲜为人知且/或容易被忘记的事实是,最派生类构造函数直接调用其(继承的)虚基类构造函数,并且如果它没有在基础成员初始化列表中显式这样做,则将调用虚基类的默认构造函数。但是(这更加晦涩难懂),对于隐式定义或声明为默认的复制/移动构造函数,所选择的虚基类构造函数不是默认构造函数,而是相应的复制/移动构造函数。如果此构造函数被删除或不可访问,则最派生类的复制/移动构造函数将被定义为已删除。
以下是一个示例(可以追溯到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

这里的B对应于basic_iosC对应于ifstreamD对应于您的BinFile;在演示中,basic_istream是不必要的。

如果取消了D的手动复制构造函数的注释,则程序将编译,但它将调用B::B(),而不是B::B(int)。这就是为什么继承没有明确给予许可的类是一个坏主意的原因之一:如果将其作为最派生类构造函数调用,则可能没有调用与您正在继承的类的构造函数调用相同的虚基类构造函数。

至于你可以做什么,我认为手写移动构造函数应该可以工作,因为在libstdc++和libcxx中,basic_ifstream的移动构造函数不会调用basic_ios的非默认构造函数(有一个从basic_streambuf指针来的),而是在构造函数体中初始化它(看起来就像[ifstream.cons]/4所说的那样)。值得阅读通过继承扩展C ++标准库?以了解其他可能的要点。


值得一提的是,您实际上无法继承移动构造函数(它们会被重载决议丢弃),而这也无法避免虚基类的初始化。 - Davis Herring
@DavisHerring 我不明白你在说什么。如果我用 unique_ptr<int> 替换 ifstream,移动构造函数就能正常工作。 - lvella
在这种情况下,我明确地说了 using std::ifstream::ifstream;,构造虚基类应该很明显,不是吗?我的意思是,C++11引入了这个语法,似乎只是“忘记”了这个角落的情况。 - lvella
@lvella:如果没有虚继承,派生类的默认移动构造函数将调用一个必需的、对应的基类构造函数,因此它看起来像是“继承”了。至于语法,它应该涵盖继承构造函数的类直接从多个基类派生的情况。 - Davis Herring
@Ivella using std::ifstream::ifstream;并不提供拷贝/移动构造函数,它只允许你从基类的glvalue以及其他构造函数参数列表中构造派生类。拷贝构造函数仍需要合成,而且即使使用继承构造函数,将初始化虚基类的责任交给直接基类也是奇怪的 - 如果在钻石继承中继承了两个基类的构造函数,哪一个应该负责初始化共享的虚基类? - ecatmur

0
正如之前的回答所提到的,基类中定义了构造函数为已删除。 这意味着你不能使用它 - 参见<istream>
__CLR_OR_THIS_CALL basic_istream(const basic_istream&) = delete;
basic_istream& __CLR_OR_THIS_CALL operator=(const basic_istream&) = delete;

而且 return ret 尝试使用已删除的复制构造函数而不是移动构造函数。

但是,如果您创建自己的移动构造函数,则应该可以解决:

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

}

你可以在你的问题评论中看到这个(@igor-tandetnik)。


网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接