sprintf
或stringstream
格式化字符串很烦人,这促使我编写了自己的小Formatter
类。我很好奇这个类的实现或它的使用是否引入了任何未定义的行为。尤其是此行是否已完全定义:
Reject( Formatter() << "Error Recieved" << 42 << " " << some_code << " '" << some_msg << "'");
我相信可以,但是我希望得到同行评审。
关注的三个主要问题:
在单个序列点内分配?是UB吗?
我对临时期限有疑问吗?
我的
Formatter
类(或它的预期用途)是否引入了任何UB?Formatter
类同时具有( operator<<
和operator std::string
。目的是将Formatter()
类用作使用std::string
的任何函数的const std::string&
参数的临时类。这是类的定义:
class Formatter
{
public:
Formatter() {};
template<class Field> Formatter& operator<<(Field f)
{
ss_ << f;
return *this;
}
operator std::string() const { return ss_.str(); }
private:
std::stringstream ss_;
};
这是一个完整的测试工具,包括上面的定义。您应该能够按原样编译和运行。您看到UB了吗?
#include <cstdlib>
#include <string>
#include <sstream>
#include <iostream>
class Formatter
{
public:
Formatter() {};
template<class Field> Formatter& operator<<(Field f)
{
ss_ << f;
return *this;
}
operator std::string() const { return ss_.str(); }
private:
std::stringstream ss_;
};
void Reject(const std::string& msg)
{
std::cout << "Recieved Message: '" << msg << "'" << std::endl;
}
int main()
{
const char& some_code = 'A';
const char* some_msg = "Something";
Reject( Formatter() << "Error Recieved" << 42 << " " << some_code << " '" << some_msg << "'");
}
#1 楼
除了已经说过的内容之外,我还将:将字符串流标记为公共。这不会影响您的代码的大多数使用,并且已经可以使用自定义操纵器进行破解以获取“内部”流对象,但是它将使那些需要访问内部流的对象(例如避免使用字符串)成为可能。复制stringstream接口中固有的内容),并知道允许他们想要的实现的细节。当然,0x move语义可以缓解这种需求,但是在返回字符串之前,请先检查流。如果它处于失败状态,则引发异常(或至少在返回字符串之前将条件记录在某处)。对于大多数用途来说,这种情况不太可能发生,但是,如果确实发生这种情况,您会很高兴地发现流失败了,而不是搞砸了格式化,同时又想知道为什么“它根本不起作用”。 />关于双重分配,根本没有分配。顺序点应该基本上是人们期望的,但是,确切地说,它看起来像: br />
Formatter()和expr_a都在标记为1的插入之前发生。
上面加上插入1,加上expr_b在插入2之前发生。插入2,加上expr_c发生在插入3之前。在调用some_function之前发生。
要添加有关临时对象的讨论,所有创建的临时对象都在表达式中:
some_function(((Formatter() << expr_a) << expr_b) << expr_c);
// 1 2 3
直到包含some_function调用的完整表达式的末尾,它们才会被销毁,这意味着不仅字符串将被传递给some_function,而且some_function届时将已经返回。 (否则将抛出异常,并且在展开时它们将被销毁,等等。)
为了处理所有操纵器,例如std :: endl,请添加:
some_function(Formatter() << make_a_temp() << "etc.")
// one temp another temp and so on
我已经多次使用这种模式来包装流,并且它非常方便,因为您可以内联使用它(如您所做的那样),或者创建Formatter变量来进行更复杂的操作(考虑循环)根据条件插入流中等)。尽管仅当包装器执行的功能比您在此处执行的操作要多时,后一种情况才重要。 :)
#2 楼
只需三个注释:我将删除空的构造函数。
如何处理std :: manipulators?
您是否不想通过const传递字段值
。
template<class Field> Formatter& operator<<(Field const& f)
// ^^^^^^
您的关注点:
是否有在单个序列点内进行双重分配?是UB吗?
没看见。
看起来不错。
我有问题吗?
没有不要这样。
评论
\ $ \ begingroup \ $
机械手有什么问题?您仍然可以为其使用模板化插入运算符。 Formatter()<< std :: hex;。你是说像Formatter()。width(10)之类的东西吗?
\ $ \ endgroup \ $
–威廉姆特
2011-1-27的3:43
\ $ \ begingroup \ $
@wilhelmtell:实际上我在考虑std :: endl。但这可能是设计使然。尽管在Formatter对象的上下文中使用std :: endl可能没有多大意义,但仍可能使一些仅将旧代码移植的用户混淆,这些旧代码与std :: endl一起使用时将无法编译。
\ $ \ endgroup \ $
–马丁·约克
2011年1月27日5:24
\ $ \ begingroup \ $
机械手无法直接工作。您需要手动添加对它们的支持。看看弗雷德·努克(Fred Nurk)的答案
\ $ \ endgroup \ $
–DavidRodríguez-dribeas
2011年1月27日15:25
\ $ \ begingroup \ $
@DavidRodríguez-dribeas:这正是我提到它的原因。
\ $ \ endgroup \ $
–马丁·约克
2011年1月27日19:40
\ $ \ begingroup \ $
约克,我应该在此添加一些@wilhelmtell。评论不是关于您的答案,而是关于他的评论。对困惑感到抱歉 :)
\ $ \ endgroup \ $
–DavidRodríguez-dribeas
2011年2月2日,12:15
#3 楼
我认为您可能会担心临时的终身问题。 (我知道我已经基于类似的代码。)作为
Reject
的参数创建的临时对象将有一个生命周期绑定到它所创建的表达式。(在C ++标准中)。因此,即使您的转换运算符返回值,它们也将在包含Reject
的表达式之后被销毁。评论
\ $ \ begingroup \ $
+1:我还担心单个序列点中的双重分配。
\ $ \ endgroup \ $
– John Dibling
2011-1-26 22:55
#4 楼
我将使用
std::ostringstream
,因为您没有使用(似乎需要)std::istringstream
的格式化功能。格式化失败时会发生什么?您可以检查失败吗?
#5 楼
看起来还可以空的ctor是不必要的;编译器生成一个就可以了。 “已接收”应拼写为“已接收” :)#6 楼
看起来不错不必太担心,最终会过度设计一个简单的解决方案:)编辑:
实际上,我会将
operator<<
的参数设置为const引用。 />评论
\ $ \ begingroup \ $
我同意!我希望没有问题,因为Formatter确实是一个很小的小发明。
\ $ \ endgroup \ $
– John Dibling
2011年1月26日在22:17
评论
\ $ \ begingroup \ $
危险是expr_c可能与expr_a同时发生。
\ $ \ endgroup \ $
–重复数据删除器
2015年10月20日,11:49