65.9K
CodeProject 正在变化。 阅读更多。
Home

iostream 修改器

starIconstarIconstarIconstarIcon
emptyStarIcon
starIcon

4.60/5 (7投票s)

2002 年 7 月 15 日

7分钟阅读

viewsIcon

127871

downloadIcon

413

通过流修改器探索扩展 iostreams 框架

概述

在写了一篇关于 iostream 插入器和提取器的文章之后,我决定写一篇关于修改器的文章。  大多数人在不自知的情况下使用 iostream 修改器。  std::endl 是最常见的修改器,它只是一个可以传递到流中影响输出的项。

std::endl

我们将从挑剔std::endl 开始。  你多久会看到这样的代码?

 for(int i = 0; i < 100; ++i) myStream << "This is function 
            number " << lNum << " " << m_szArray[i] << endl;

常常不为人所知的是 std::endl 不仅仅是传递一个换行符。  它还会向流发送一个flush,这在上面那样使用时可能会严重影响性能。  基本上,一个缓冲流会在调用flush 或流被销毁时被卸载。  在上面的例子中,缓冲的优势几乎完全丧失了。  因此,作为一个简单的第一个例子,我将展示如何创建一个新的修改器,它执行换行而不执行刷新。  它看起来像这样:

#include <iosfwd>

class Newline
{
public:
    friend std::ostream & operator <<(std::ostream &os, const Newline nl)
    {
        os << "\r\n";
        return os;
    }
};

这样我们就可以像这样创建新行:

cout << "Here is my line " << Newline();

它的工作原理是这样的。  我们的代码创建一个新的、未命名的类的实例。  在它(短暂的)生命周期中,流插入器被调用,实例被传入。  我们对流运算符的实现是基本级的,它只是传入新行并返回流对象(正如它应该的那样)。 

开始嗨起来

前面的例子为了说明任何修改器都需要具备的核心功能而被故意简化了。  我们的下一个例子将更复杂一些:它将接受参数。  在我的工作中,我经常与数据库交互,并且经常使用ostringstream 构建查询。  在那种情况下,我经常根据日期查询一个范围,所以我编写了一个修改器来插入一个日期戳。  我们现在一步一步地构建这个修改器。

常量

我们需要的第一件事是传递给构造函数的值。  我只构建了四种,但你可以轻松地添加更多。  一些显而易见的添加项是 dd/mm/yyyy 和 mm/dd/yyyy 格式。  对我来说,我们通常想要 ISO8601 格式,带或不带时间,我还添加了一个包含月份文本格式的,这是另一种普遍可读的格式(即它不会引起混淆,不知道哪个数字是月份,哪个是日期)。  经过一番权衡,我选择将此代码包含在 namespace std 中,因为虽然总的来说我不会使用 std 作为我想用命名空间保护起来的东西的倾倒场,但这段代码确实与 std 库直接交互,并且将其与 std 放在一起是合乎逻辑的。

namespace std
{
    const int DT_ISO8601            = 1;
    const int DT_ISO8601DateOnly    = 2;
    const int DT_DD_MMM_YYYY        = 3;
    const int DT_HH_MM              = 4;
    
    const std::string DateStamp::Dates [12] = 
    {
        "Jan.", "Feb.", "March", "Apr.", "May", "June", 
        "July", "Aug", "Sept.", "Oct.", "Nov.", "Dec."
    };

构造函数

我们的构造函数将使用 explicit 关键字来确保不发生任何隐式转换,以值传递给它。  所有参数都有默认值,因此很容易构建我们首选的日期戳格式。  参数是格式常量、一个偏移量(所以你可以传入 -7 来获取一周前的日期,或 1 来获取明天的日期)以及用于日期元素和时间元素之间的分隔符。  请注意,我使用了字符串而不是字符,这样你就可以拥有多字符分隔符(如果你愿意的话)。

class DateStamp
    {
    public:
        explicit DateStamp(int nType = DT_ISO8601, int nOffset = 0, 
                           std::string sDateDelimiter = "-", 
                           std::string sTimeDelimiter = ":") 
            : datetype(nType), dateDelimiter(sDateDelimiter), 
              timeDelimiter(sTimeDelimiter), offset(nOffset) {}

插入器

插入器的核心部分相当明显且无趣。  我使用 ::time 和 ::localtime 来获取 tm 结构,其中包含时间戳,然后根据格式值进行 switch,以便知道要流式传输到 stringstream 中什么内容,然后我们将它转储到目标流中。  最重要的事情可能是要注意它是在类内部定义的,并且被定义为类的 friend。  这意味着我们可以使我们的变量私有,并且仍然可以在插入器中访问它们。

 friend std::ostream & operator  <<(std::ostream &os, const DateStamp &mm) 
{
    time_t lTime; ::time(&lTime); 
    lTime += mm.offset * 86400; // 86400 seconds in a day 
    tm* ptmDate = ::localtime(&lTime); 
    
    std::ostringstream ss;
                        
    ss.fill('0'); 
    switch(mm.datetype) 
    { 
    case DT_ISO8601: 
    case DT_ISO8601DateOnly: 
        ss << ptmDate->tm_year + 1900 << mm.dateDelimiter;
        ss << std::setw(2) << ptmDate->tm_mon + 1 << mm.dateDelimiter;
        ss << std::setw(2) << ptmDate->tm_mday;
                
        if (mm.datetype == DT_ISO8601DateOnly) break; 
        
        ss << "T" << std::setw(2) << ptmDate->tm_hour << mm.timeDelimiter; 
        ss << std::setw(2) << ptmDate->tm_min << mm.timeDelimiter; 
        ss << std::setw(2) << ptmDate->tm_sec; 
        break; 
    case DT_DD_MMM_YYYY: 
        ss << std::setw(2) << ptmDate->tm_mday << mm.dateDelimiter; 
        ss << Dates[ptmDate->tm_mon] << mm.dateDelimiter 
           << ptmDate->tm_year + 1900; 
        break;
    case DT_HH_MM:
        ss << std::setw(2) << ptmDate->tm_hour << mm.timeDelimiter;
        ss <<    std::setw(2) << ptmDate->tm_min;
        break; 
    }
    
    os << ss.str();
    return os;
};

带状态的修改器

上面的日期修改器都通过传递 first std::setw(2) 来强制月份或日期小于 10 的前面加一个 0。  这是如何工作的?  显然,在传递修改器时,iostreams 尚未知道需要强制为该宽度的值。  答案是修改器可以有状态。  这是一件很棒的事情,它的工作原理如下。  当我们编写一个需要有状态的修改器时,我们在类声明中放置一个静态 int,并使用对 std::ios_base::xalloc() 的调用来初始化它。  这会返回一个 int,在每次运行时它可能会有所不同。  我认为它是一个指向链表的索引(仅仅因为它最合乎逻辑),但无论它是什么,它都可以用于使用 iwordpword 函数来设置和获取 void * 或整数,这两个函数存在于你传入的流对象中,但值是通过 iostreams 共享的,所以如果你想设置或获取一个值而没有流可以使用,你也可以使用 cout,或任何其他标准流,甚至构造一个(尽管我不知道你为什么会这样做)。  为了说明这一点,我们最后的例子是一个简单的类,旨在保存一个人的姓名详细信息。  构造函数接受两个字符串,一个名字和一个姓。  我们再次将它们放在 std 中,尽管我通常会将它们放在别处,但我预计除了作为示例外,没有人会使用这个例子。  我们还为我们将能够使用的格式设置了值。

namespace std
{
    const int NAME_FIRST_LAST    = 1;
    const int NAME_LAST_FIRST    = 2;
    const int NAME_INITAL_LAST   = 3;
    const int NAME_INITIALS      = 4;

    class CName
    {
    friend class NameFormat;
    public:
        CName(std::string first, std::string last)
                            : m_sFirst(first), m_sLast(last)
        {};

到目前为止,一切都相当直接。  下一行是事情变得有趣的地方。  为了清晰起见,我将变量显示在类作用域内,然后显示如何声明其值在类外部。

class CName
{
...
    static int GetAlloc(){return m_nAlloc;}; 
private:
    std::string m_sFirst, m_sLast; 
    static const int m_nAlloc;
...
}

const int CName::m_nAlloc = std::ios_base::xalloc();     

正如我已经提到的,对 xalloc 的调用返回一个索引,我们将使用这个索引通过 iostreams 传递值。  我说“通过”,因为事实上我们的修改器将使用这个索引来存储一个值,而类的插入器将提取它并用它来决定如何格式化输出。  因此,这个值属于类还是修改器,这取决于个人品味。  我选择类,因为修改器也定义在类里面。  所以在类定义内部,我们这样定义我们的插入器:

friend std::ostream & operator <<(std::ostream &os, const CName &nm)
{
    if (!os.good())
        return os;

    std::ostream::sentry sentry(os);

    if(sentry)
    {
        std::ostringstream ss;

        switch(os.iword(nm.m_nAlloc))
        {
        default:
        case NAME_FIRST_LAST:
            ss << nm.m_sFirst << " " << nm.m_sLast;
            break;
        case NAME_LAST_FIRST:
            ss << nm.m_sLast << ", " << nm.m_sFirst;
            break;
        case NAME_INITAL_LAST:
            ss << nm.m_sFirst[0] << ". " << nm.m_sLast;
            break;
        case NAME_INITIALS:
            ss << nm.m_sFirst[0] << nm.m_sLast[0];
            break;
        }

        os << ss.str();
    }

    return os;
};

如果你想了解更多关于编写 iostream 插入器和提取器的方法,请参阅我关于该主题的文章。

剩下要做的就是定义流修改器,它将设置存储在 iostreams 中并由我们的插入器检索的值。

class NameFormat
{
public:
    explicit NameFormat(int nFormat) : m_nFormat(nFormat){}
    template<class charT class Traits class="",>
    friend std::basic_ostream<charT Traits,> & operator <<  
                                 (std::basic_ostream<charT Traits ,>& os, 
                                 const NameFormat & nf)
    {
        os.iword(CName::GetAlloc()) = nf.m_nFormat;
        return os;
    }
private:
    int m_nFormat;     
};

就这样。  我们现在可以通过传入不同的格式来打印相同的姓名,只需通过我们的修改器传入即可。  这是示例程序的完整列表:

#include "stdafx.h"
#include 

#include "Date Inserter.h"
#include "Newline.h"
#include "Name.h"

using std::cout;
using std::cin;
using std::endl;
using std::CName;
using std::NameFormat;
using std::DateStamp;

int _tmain(int argc, _TCHAR* argv[])
{
    cout << "My output " << endl;
    cout << "My other output " << Newline();
    cout << "And some more.... " << Newline();
    CName name("Christian", "Graus");
    cout << name << Newline();
    cout << NameFormat(std::NAME_LAST_FIRST) << name << Newline();
    cout << NameFormat(std::NAME_INITAL_LAST) << name << Newline();
    cout << NameFormat(std::NAME_INITIALS) << name << Newline();
    cout << DateStamp() << Newline();
    cout << DateStamp(std::DT_ISO8601DateOnly) << Newline();
    cout << DateStamp(std::DT_DD_MMM_YYYY) << Newline();
    cout << DateStamp(std::DT_HH_MM) << Newline();
    int i;
    cin >> i;
    return 0;
}

输出看起来像这样:

My output
My other output
And some more....
Christian Graus
Graus, Christian
C. Graus
CG
2002-07-15T20:19:59
2002-07-15
15-July-2002
20:19

我希望你同意我的看法,iostream 是一个高度灵活且有用的框架。  添加定义自己的流类型的能力,你就拥有了一个可以轻松地将任何你想要的信息传递到任何你想去的地方的系统。  我的批评者经常提出 iostream 会增加大约 80KB 可执行文件大小的事实。  这可能是真的(也就是说,我还没有检查过,但我相信告诉我的人)。  然而,这对我来说几乎不是问题,因为我*经常*使用 iostream,所以我的 80k 获得了很大的效益。  为了保持账户的平衡,我提到这一点,以便你在决定是否在特定项目上使用它们时能够意识到这一点。

© . All rights reserved.