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_;
};
在我看来,这对于一次性使用来说太复杂了,除非它会被大量代码使用,否则我会简单地放弃带有右值引用的构造函数而不是修复它。
以下代码给了我一些意想不到的行为:
#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_;
};
在我看来,这对于一次性使用来说太复杂了,除非它会被大量代码使用,否则我会简单地放弃带有右值引用的构造函数而不是修复它。