在操作std::cout后恢复其状态

143
Suppose我有这样的代码:
void printHex(std::ostream& x){
    x<<std::hex<<123;
}
..
int main(){
    std::cout<<100; // prints 100 base 10
    printHex(std::cout); //prints 123 in hex
    std::cout<<73; //problem! prints 73 in hex..
}

我的问题是,是否有任何方法在从函数返回后“恢复”cout的状态?(有点像 std::boolalphastd::noboolalpha..)?

谢谢。


我相信十六进制只在下一次移位操作中持续。如果您手动更改格式标志而不是使用操作器,则更改仅为持久性。 - Billy ONeal
5
不,使用流控制器与手动更改格式标志具有相同的效果。:-P - C. K. Young
3
如果你因为Covertiy找到了“未恢复ostream格式(STREAM_FORMAT_STATE)”而来到这里,请参见Coverity finding: Not restoring ostream format (STREAM_FORMAT_STATE) - jww
我做了类似的事情 - 请看我的代码审查问题:使用标准流,在之后恢复其设置 - Toby Speight
2
这个问题是iostream不比stdio更好的完美例证。因为没有持久的iomanip,我发现了两个令人讨厌的bug。 - fuujuhi
就这个问题而言,我刚刚发现当使用g++ 4.9.0和-std=c++11编译时,std::setw仅影响下一个移位操作。 - uuu777
9个回答

130

你需要添加#include <iostream>#include <ios>,然后在需要的时候调用:

std::ios_base::fmtflags f( cout.flags() );

//Your code here...

cout.flags( f );

你可以将这些放在函数的开头和结尾,或者查看这个答案,了解如何使用RAII来实现。


5
@ChrisJester-Young,实际上好的C++是RAII,特别是在这种情况下! - Alexis Wilke
4
@Alexis 我完全同意。看看我的答案(Boost IO Stream State Saver)。 :-) - C. K. Young
3
这不是异常安全的。 - einpoklum
5
除了标志位外,流状态还包括其他内容。 - jww
5
您可以避免问题,方法是不要将格式强制推送到数据流中。将格式和数据推送到临时stringstream变量中,然后再进行打印。 - Mark Sherred
显示剩余4条评论

84
请注意,这里提供的答案不会完全恢复std::cout的状态。例如,调用.flags()std::setfill仍将保持“粘性”状态。更好的解决方案是使用.copyfmt:
std::ios oldState(nullptr);
oldState.copyfmt(std::cout);

std::cout
    << std::hex
    << std::setw(8)
    << std::setfill('0')
    << 0xDECEA5ED
    << std::endl;

std::cout.copyfmt(oldState);

std::cout
    << std::setw(15)
    << std::left
    << "case closed"
    << std::endl;

将打印:

case closed

与其...不如...

case closed0000

尽管我的原始问题几年前已经得到了回答,但这个答案是一个很好的补充。 :-) - UltraInstinct
8
看起来这是一个更好的解决方案,因此,您可以并且应该将其作为被接受的答案。 - underscore_d
由于流启用了异常,因此出于某些原因会抛出异常。http://coliru.stacked-crooked.com/a/2a4ce6f5d3d8925b - anton_rh
1
看起来 std::ios 始终处于坏状态,因为它具有 NULL rdbuf。因此,启用异常抛出的状态设置会导致异常抛出。解决方案:1)使用某个类(例如 std::stringstream),其中 rdbuf 已设置为 std::ios,而不是 std::ios。2)将异常状态单独保存到局部变量中,并在 state.copyfmt 之前禁用它们,然后从变量中恢复异常(并在从禁用异常的 oldState 恢复状态后再次执行此操作)。3)像这样将 rdbuf 设置为 std::iosstruct : std::streambuf {} sbuf; std::ios oldState(&sbuf); - anton_rh

63

Boost IO Stream State Saver似乎正是您需要的。 :-)

根据您的代码片段示例:

void printHex(std::ostream& x) {
    boost::io::ios_flags_saver ifs(x);
    x << std::hex << 123;
}

17
但是它是异常安全的,与其他答案不同。;-) - C. K. Young
2
流状态除了标志位之外还有更多内容。 - jww
4
IO 流状态保存库有多个类,用于保存流状态的不同部分,其中 ios_flags_saver 只是其中之一。 - C. K. Young
1
为什么每遇到一个小问题就要链接 Boost 库...让我休息一下吧 :D - Dumbo
3
如果你认为重新实现和维护每一个小细节都值得,而不是使用经过检查、经过充分测试的库... - jupp0r
显示剩余2条评论

24
我已经使用这个答案中的示例代码创建了一个RAII类。这种技术的重要优势在于,如果函数在iostream上设置标志并具有多个返回路径,则无论使用哪个返回路径,析构函数总是会被调用并且标志总是会被重置。当函数返回时,不会遗漏恢复标志的机会。
class IosFlagSaver {
public:
    explicit IosFlagSaver(std::ostream& _ios):
        ios(_ios),
        f(_ios.flags()) {
    }
    ~IosFlagSaver() {
        ios.flags(f);
    }

    IosFlagSaver(const IosFlagSaver &rhs) = delete;
    IosFlagSaver& operator= (const IosFlagSaver& rhs) = delete;

private:
    std::ostream& ios;
    std::ios::fmtflags f;
};

每当您想要保存当前标志状态时,您可以通过创建IosFlagSaver的本地实例来使用它。当此实例超出范围时,标志状态将被恢复。

void f(int i) {
    IosFlagSaver iosfs(std::cout);

    std::cout << i << " " << std::hex << i << " ";
    if (i < 100) {
        std::cout << std::endl;
        return;
    }
    std::cout << std::oct << i << std::endl;
}

2
很好,如果有人抛出异常,你仍然可以在你的流中得到正确的标志。 - Alexis Wilke
4
流状态除了标志位以外还有其他内容。 - jww
1
我真的希望C++允许try/finally。这是一个很好的例子,RAII可以解决问题,但finally会更简单。 - Trade-Ideas Philip
2
如果你的项目至少有点理智,那么你应该使用Boost库,它提供了状态保存器来实现这个目的。 - Jan Hudec

14

您可以在标准输出缓冲区周围创建另一个包装器:

#include <iostream>
#include <iomanip>
int main() {
    int x = 76;
    std::ostream hexcout (std::cout.rdbuf());
    hexcout << std::hex;
    std::cout << x << "\n"; // still "76"
    hexcout << x << "\n";   // "4c"
}

在一个函数中:

void print(std::ostream& os) {
    std::ostream copy (os.rdbuf());
    copy << std::hex;
    copy << 123;
}

当然,如果性能是一个问题,这会更昂贵,因为它复制整个ios对象(但不包括缓冲区),包括一些你付费但不太可能使用的东西,比如语言环境。

否则,我觉得如果你要使用.flags(),最好保持一致,也使用.setf()而不是<<语法(纯粹是风格问题)。

void print(std::ostream& os) {
    std::ios::fmtflags os_flags (os.flags());
    os.setf(std::ios::hex);
    os << 123;
    os.flags(os_flags);
}

如其他人所述,您可以将上述内容(以及.precision().fill(),但通常不包括通常不会被修改且更重的区域设置和单词相关内容)放入一个类中以方便使用,并使其具有异常安全性;构造函数应接受std::ios&


好的观点[+], 但是当然要记得使用std::stringstream来进行格式化部分,正如Mark Sherred指出的那样 - Wolf
当然,这两种格式化输出的方法都是有效的,它们都引入了一个流对象,你所描述的那个对我来说是新的。现在我必须考虑一下优缺点。不过,这是一个启发性问题,有启发性的答案...(我的意思是流复制变量)。 - Wolf
2
你不能复制一个流,因为拷贝缓冲区通常没有意义(如 stdout)。但是,你可以为同一缓冲区拥有多个流对象,这就是本回答所建议的方法。而 std::stringstream 则会创建它自己独立的 std::stringbuf(一个 std::streambuf 衍生类),然后需要将其倒入 std::cout.rdbuf() - n.caillou
@n.caillou “The” std::ostream 实例通常由客户端提供(或者是全局的,例如 std::cout),并且需要维护/恢复客户端/全局 ostream 状态。本地/自动 std::stringstream 允许您将状态调整隔离到本地 std::ostream 中,而不是操纵客户端的 std::ostream(或 cout)状态。 - franji1
1
@franji1,你可以不用创建stringbuf来实现。你没有理解答案;流和缓冲区是两个不同的东西。 - n.caillou
显示剩余2条评论

10

C++20的std::format将成为在大多数情况下替代save restore的更优选择

一旦您可以使用它,您将能够简单地编写十六进制,例如:

#include <format>
#include <string>

int main() {
    std::cout << std::format("{:x} {:#x} {}\n", 16, 17, 18);
}

期望输出:

10 0x11 18

这将完全解决修改std::cout状态的疯狂行为。

现有的fmt库实现了它,以在官方支持之前使用:https://github.com/fmtlib/fmt 在Ubuntu 22.04上安装:

sudo apt install libfmt-dev

修改源代码以替换:

  • <format><fmt/core.h>
  • std::formatfmt::format

main.cpp

#include <iostream>

#include <fmt/core.h>

int main() {
    std::cout << fmt::format("{:x} {:#x} {}\n", 16, 17, 18);
}

编译并运行的命令为:

g++ -std=c++11 -o main.out main.cpp -lfmt
./main.out

输出:

10 0x11 18

相关: 类似于sprintf的std::string格式化


3
了解这点很好,但截至2021年4月,尽管已经有了标准,编译器仍不支持它,也许在这个答案中提到这一点是值得的。 - Victor Polevoy
1
我非常期待这个库能够成为C++编译器的标准发行版。 - shuhalo
与此同时,标准已经发布,并且也有支持它的实现。我所经历的最令人印象深刻的事情是,在编译时发生的错误会指示格式字符串和参数之间的不匹配。 - Wolf
同时,标准已经发布,并且也有支持它的实现。我所经历的最令人印象深刻的事情是,编译时错误指示了格式字符串和参数之间的不匹配 - undefined

9

稍作修改以使输出更易读:

void printHex(std::ostream& x) {
   ios::fmtflags f(x.flags());
   x << std::hex << 123 << "\n";
   x.flags(f);
}

int main() {
    std::cout << 100 << "\n"; // prints 100 base 10
    printHex(std::cout);      // prints 123 in hex
    std::cout << 73 << "\n";  // problem! prints 73 in hex..
}

1

不要使用<<的方式将格式注入到cout中,而是采用setfunsetf可能会是更加清晰的解决方案。

void printHex(std::ostream& x){
  x.setf(std::ios::hex, std::ios::basefield);
  x << 123;
  x.unsetf(std::ios::basefield);
}

ios_base 命名空间也很好用

void printHex(std::ostream& x){
  x.setf(std::ios_base::hex, std::ios_base::basefield);
  x << 123;
  x.unsetf(std::ios_base::basefield);
}

参考资料:http://www.cplusplus.com/reference/ios/ios_base/setf/


0

我想对qbert220的回答进行一些概括:

#include <ios>

class IoStreamFlagsRestorer
{
public:
    IoStreamFlagsRestorer(std::ios_base & ioStream)
        : ioStream_(ioStream)
        , flags_(ioStream_.flags())
    {
    }

    ~IoStreamFlagsRestorer()
    {
        ioStream_.flags(flags_);
    }

private:
    std::ios_base & ioStream_;
    std::ios_base::fmtflags const flags_;
};

这应该适用于输入流和其他类型。

附注:我本来想把这个简单地作为对上面答案的评论,但由于声望不足,stackoverflow不允许我这样做。因此,我只能在这里发布答案,而不是一个简单的评论...


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