c++ std::stringstream 给我奇怪的行为

c++ std::stringstream gives me weird behavior

以下代码给了我一些意想不到的行为:

#include <map>
#include <iostream>
#include <string>
#include <sstream>

const std::string data1 =
"column1        column2\n"
"1      3\n"
"5      6\n"
"49     22\n";

const std::string data2 =
"column1        column2 column3\n"
"10     20      40\n"
"30     20      10\n";

class IOLoader
{
public:
        // accept an istream and load the next line with member Next()
        IOLoader(std::istream& t_stream) : stream_(t_stream) 
        { 
                for(int i = 0; i < 2; ++i) std::getline(stream_, line_);
        };// get rid of the header

        IOLoader(std::istream&& t_stream) : stream_(t_stream) 
        { 
                for(int i = 0; i < 2; ++i) std::getline(stream_, line_);
        };// get rid of the header

        void Next()
        {
                // load next line
                if(!std::getline(stream_, line_))
                        line_ = "";
        };

        bool IsEnd()
        { return line_.empty(); };

        std::istream& stream_;
        std::string line_;
};

int main()
{
        for(IOLoader data1_loader = IOLoader((std::stringstream(data1))); !data1_loader.IsEnd(); data1_loader.Next())
        {
                std::cout << data1_loader.line_ << "\n";

                // weird result if the following part is uncommented
                /*
                IOLoader data2_loader = IOLoader(std::stringstream(data2));
                std::cout << data2_loader.line_ << "\n";
                data2_loader.Next();
                std::cout << data2_loader.line_ << "\n";
                */
        }
}

我希望 class IOLoader 逐行读取字符串。我得到以下没有注释部分的结果:

1       3
5       6
49      22

这完全在意料之中。问题是当我用 data2_loader 取消注释部分时会发生什么。现在它给了我:

1       3
10      20      40
30      20      10
mn349   22
10      20      40
30      20      10

我不知道发生了什么。这是我最初的预期:

1       3
10      20      40
30      20      10
5       6
10      20      40
30      20      10
49      22
10      20      40
30      20      10

无论出于何种原因,如果我使用 data2 创建字符串流,都无法正确读取 data1。我用 g++ 4.9.2 编译它。非常感谢您的帮助。

当您编写 IOLoader data1_loader = IOLoader((std::stringstream(data1))); 时,您将 IOLoader::stream_ 引用成员绑定到 临时 ,因为 std::stringstream(data1) 在之后被销毁构造函数。您只能从对已销毁对象的悬垂引用中读取数据,这是未定义的行为,结果绝对可能发生任何事情。一个简单的解决方法是将 stringstream 声明为只要 IOLoader 需要它们就可以存在的变量,并删除你的 IOLoader(std::istream&& t_stream) 构造函数,因为它实际上并没有移动 t_stream],作为 r 值参考,通常是临时的。

std::stringstream ss1 {data1};
for(IOLoader data1_loader = IOLoader(ss1); !data1_loader.IsEnd(); data1_loader.Next()){
    std::cout << data1_loader.line_ << "\n";

    std::stringstream ss2 { data2 };
    IOLoader data2_loader = IOLoader(ss2);
    std::cout << data2_loader.line_ << "\n";
    data2_loader.Next();
    std::cout << data2_loader.line_ << "\n";
}

如果您需要 IOLoader 非常普遍地处理您无法承担所有权的流,例如 std::cin,那么坚持使用参考成员是有意义的。请注意,只要使用 stream_ 成员,引用的流就需要存在。否则,如果您只使用 std::stringstream,那么最简单的方法就是获取流的所有权并使 IOLoader::stream_ 成为值类型。例如,您可以 std::move 通过右值引用传递给构造函数的流。

传递右值引用并保留它是错误的,并且几乎肯定会导致未定义的行为 (UB)。我指的是下面的代码,这有助于但不会直接导致UB:

IOLoader(std::istream&& t_stream) : stream_(t_stream) 
{ 
      for(int i = 0; i < 2; ++i) std::getline(stream_, line_);
};// get rid of the header

构造函数使以下行可以静默触发 UB:

for(IOLoader data1_loader = IOLoader((std::stringstream(data1))); !data1_loader.IsEnd(); data1_loader.Next())

此行创建一个临时(右值)stringstream 对象。由于这是一个右值,它的引用被愉快地传递给接受右值引用的 IOLoader 的构造函数。但是接受右值引用的构造函数并没有移动任何东西,只是简单地存储了一个对临时 stringstream 的引用。这与右值引用的正常使用相反,即移动对象。到循环体开始时,临时 stringstream 已经被销毁, stream_ 指的是一个被销毁的对象。在 Next() 中或以任何其他方式使用此类引用是 UB。

您可以通过创建命名的 stingsstream 对象来修复该错误的特定实例:

std::stringstream tmp_stream(data1);
for(IOLoader data1_loader = IOLoader(tmp_stream); !data1_loader.IsEnd(); data1_loader.Next())

这将修复实例,但不会修复核心问题。核心问题是存在误导性的 && 构造函数。 && 构造函数有两个选项,要么完全删除它,要么让它实际移动 stringstream:

class IOLoader
{
...
        IOLoader(std::stringstream&& t_stream) : saved_stream_(std::move(t_stream)), stream_(saved_stream_)
        { 
                for(int i = 0; i < 2; ++i) std::getline(stream_, line_);
        };// get rid of the header

...
        std::stringstream saved_stream_;
        std::istream& stream_;
        std::string line_;
};

缺点是在这种情况下,它只能与 stringstream 一起使用,而不能与 istringstream 等类似类型一起使用。您可以使用模板使其更通用(额外堆分配的运行时成本):

class IOLoader
{
public:
....    
        // enable_if avoids regular references, so that we neither prefer this ctor
        // over the other ctor, nor try to move from a regular lvalue reference.
        template <typename Stream, typename = typename std::enable_if<!std::is_reference<Stream>::value>::type>
        IOLoader(Stream&& t_stream) : saved_stream_(std::make_unique<typename std::decay<Stream>::type>(std::move(t_stream))), stream_(*saved_stream_)
        { 
                for(int i = 0; i < 2; ++i) std::getline(stream_, line_);
        };
...

        std::unique_ptr<std::istream> saved_stream_;
        std::istream& stream_;
        std::string line_;
};

在我看来,这对于一次性使用来说太复杂了,除非它会被大量代码使用,否则我会简单地放弃带有右值引用的构造函数而不是修复它。