将临时变量绑定到非const引用

16

思路

我尽量完全避免在C++代码中使用赋值操作。也就是说,除了循环变量或累加器之外,在可能的情况下(即总是)只使用初始化和将本地变量声明为 const 。

现在,我发现这种方法不适用于某些情况。我认为这是一个普遍的模式,但特别是在以下情况下会出现:

问题描述

假设我有一个程序,它将输入文件的内容加载到字符串中。您可以通过提供文件名(tool filename)或使用标准输入流(cat filename | tool)调用该工具。那么,我该如何初始化字符串呢?

以下方法行不通:

bool const use_stdin = argc == 1;
std::string const input = slurp(use_stdin ? static_cast<std::istream&>(std::cin)
                                          : std::ifstream(argv[1]));

为什么这样不起作用?因为slurp的原型应该如下所示:

std::string slurp(std::istream&);

也就是说,参数i是const的,因此我无法将其绑定到临时变量。使用另一个变量似乎也没有解决这个问题的方法。

笨拙的解决方法

目前,我使用以下解决方案:

std::string input;
if (use_stdin)
    input = slurp(std::cin);
else {
    std::ifstream in(argv[1]);
    input = slurp(in);
}

但是这让我感到不舒服。首先,它需要更多的代码(以源代码行数计算),但它也使用了一个 if 而不是(在这里)更合适的条件表达式,并且在声明后进行了赋值,而我想避免这种方式。

有没有一种好的方法来避免这种间接初始化的风格? 这个问题可能可以概括为所有需要改变临时对象的情况。在某种程度上,流难道不是为处理这些情况而设计的吗(一个const 流是没有意义的,但在临时流上操作确实是有意义的)?


1
为什么这里需要使用 static_cast - n. m.
@n.m.:编译器无法看穿 ?:: 两侧必须是相同的类型。 - David Schwartz
@VJovic 这并不是问题的关键,只是读取直到流结束,并将结果存储在一个连续的字符串中。 - Konrad Rudolph
我想主要问题在于C++并不是为这种风格而设计的。在Haskell工具中,当传递文件名时,我通过递归函数将stdin替换为文件流,但我认为这种方法在这里并不合适。 - stefaanv
@LightnessRacesinOrbit:这对std::cin管用吗? - Mike Seymour
显示剩余14条评论
4个回答

14

为什么不直接重载 slurp 函数呢?

std::string slurp(char const* filename) {
  std::ifstream in(filename);
  return slurp(in);
}

int main(int argc, char* argv[]) {
  bool const use_stdin = argc == 1;
  std::string const input = use_stdin ? slurp(std::cin) : slurp(argv[1]);
}

使用条件运算符,这是一个通用解决方案。


一个绝佳的解决方案。我目前在Python中经常使用它,但奇怪的是,我没有想到在C++中也这样做。 - James Kanze

11

在处理 argv 时,带有 if 的解决方案是更或少是标准解决方案:

if ( argc == 1 ) {
    process( std::cin );
} else {
    for ( int i = 1; i != argc; ++ i ) {
        std::ifstream in( argv[i] );
        if ( in.is_open() ) {
            process( in );
        } else {
            std::cerr << "cannot open " << argv[i] << std::endl;
    }
}

然而,这并不处理您的情况,因为您的主要关注点是获取一个字符串,而不是“处理”文件名参数。

在我的代码中,我使用了一个我编写的MultiFileInputStream,它在构造函数中接受一个文件名列表,并且只有在读取完最后一个文件时才返回EOF:如果列表为空,则读取std::cin。这为您的问题提供了一种优雅而简单的解决方案:

MultiFileInputStream in(
        std::vector<std::string>( argv + 1, argv + argc ) );
std::string const input = slurp( in );

如果您经常编写类似Unix工具程序,那么编写这个类是值得的。然而,它绝对不是简单的,如果只需要使用一次,则可能需要花费大量的工作。

一个更通用的解决方案基于以下事实:您可以对临时对象调用非const成员函数,并且std::istream的大多数成员函数都返回std::istream&——一个非const引用,然后将绑定到非const引用。因此,您总是可以编写类似以下代码:

std::string const input = slurp(
            use_stdin
            ? std::cin.ignore( 0 )
            : std::ifstream( argv[1] ).ignore( 0 ) );

我认为这是一种有点瑕疵的方法,而且它还有一个更普遍的问题,即您无法检查构造函数调用时打开std :: ifstream 是否成功。

更普遍地说,虽然我理解您试图实现什么,但我认为您会发现IO几乎总是表示异常。 您不能在没有先定义它的情况下读取int,也不能在没有先定义std :: string的情况下读取一行。 我同意这并不像可能那么优雅,但是,正确处理错误的代码很少像人们期望的那样优雅。(一种解决方案是从std :: ifstream 派生以在打开失败时引发异常; 您只需要编写一个构造函数,在其中检查is_open()


+1 我最喜欢MultiFileInputStream的解决方案。如果流API无法解决你的问题,请在其上添加一个shim。 :) - vhallac
流API确实可以解决这个问题。您只需要扩展实现即可。(“MultiFileInputStream”继承自“std :: istream”。iostreams的设计考虑了扩展,我无法想象我们没有至少一个自定义“streambuf”和任意数量自定义操纵程序的应用场景。) - James Kanze

3

所有SSA风格的语言都需要有phi节点才能使用,这是实际情况。在任何需要根据条件值从两种不同类型中构建的情况下,您都会遇到同样的问题。三元运算符无法处理这种情况。当然,在C++11中有其他技巧,例如移动流或类似的操作,或使用lambda函数。而IOstreams的设计几乎是与您尝试做的相反,所以我认为您只需做出一个例外。


谢谢,我不知道这个通用问题的名称(显然我需要重新阅读《龙书》)。iostreams 的 phi 函数确实是我需要的,移动可能是一个合适的解决方案。太棒了,学到了有趣的东西。 - Konrad Rudolph

1

另一个选择可能是使用中间变量来保存流:

std::istream&& is = argc==1? std::move(cin) : std::ifstream(argv[1]);
std::string const input = slurp(is);

利用命名右值引用是左值的事实。


我无法理解为什么这是合法的。这如何确保在作用域结束时调用ifstream的析构函数? - Konrad Rudolph
@Konrad:你是否熟悉绑定临时对象时的引用常量规则?它们在这里也适用。ifstream 临时对象的生命周期被延长,就像它被绑定到 std::istream const& 一样,并且当引用超出作用域时调用析构函数。在这里使用右值引用的优点是,您可以随意修改对象。 - Xeo
我对那个很熟悉。但是如果这适用于这里,那么如果在您的代码中将其绑定到isstd::cin的析构函数不会被调用两次吗?我的意思是,通常情况下不会,但条件表达式的确切类型是什么,以及它如何影响编译器是否将对象的生命周期绑定到is的作用域? - Konrad Rudolph
1
被销毁的对象不同。一个是原始的 X("a"),另一个是新创建的对象,它是移动操作的目标。请参见 http://ideone.com/s9L62 以获取更完整的信息。 - vhallac
1
@vhallac:谢谢,这很有道理。但太糟糕了,因为现在这个答案真的没用了...谁想要从标准输入中移动呢?:( - Xeo
显示剩余3条评论

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