自定义流操纵器,用于在任何基中流式传输整数

Custom stream manipulator for streaming integers in any base

本文关键字:整数 任何基 传输 用于 操纵 自定义      更新时间:2023-10-16

我可以使std::ostream对象输出十六进制整数,例如

std::cout << std::hex << 0xabc; //prints `abc`, not the base-10 representation

是否有任何操纵器适用于所有底座?类似的东西

std::cout << std::base(4) << 20; //I want this to output 110

如果有,那么我没有进一步的问题了。如果没有,我可以写一个吗?它不会要求我访问std::ostream的私有实现详细信息吗?

请注意,我知道我可以编写一个函数,该函数接受一个数字并将其转换为字符串,该字符串是该数字在任何基数中的表示。或者我可以使用一个已经存在的。我问的是关于自定义流操纵器——它们可能吗?

您可以执行以下操作。我对代码进行了注释,以解释每个部分正在做什么,但本质上是这样的:

  • 创建一个"操纵器"结构,使用xallociword在流中存储一些数据
  • 创建一个自定义num_put面,用于查找操纵器并应用操纵

这是代码。。。

编辑:请注意,我不确定我在这里是否正确处理了std::ios_base::internal标志,因为我实际上不知道它的用途。

编辑2:我发现了std::ios_base::internal的用途,并更新了代码来处理它。

Edit 3:添加了对std::locacle::global的调用,以显示如何使所有标准流类默认支持新的流操纵器,而不必imbue

#include <algorithm>
#include <cassert>
#include <climits>
#include <iomanip>
#include <iostream>
#include <locale>
namespace StreamManip {
// Define a base manipulator type, its what the built in stream manipulators
// do when they take parameters, only they return an opaque type.
struct BaseManip
{
    int mBase;
    BaseManip(int base) : mBase(base)
    {
        assert(base >= 2);
        assert(base <= 36);
    }
    static int getIWord()
    {
        // call xalloc once to get an index at which we can store data for this
        // manipulator.
        static int iw = std::ios_base::xalloc();
        return iw;
    }
    void apply(std::ostream& os) const
    {
        // store the base value in the manipulator.
        os.iword(getIWord()) = mBase;
    }
};
// We need this so we can apply our custom stream manipulator to the stream.
std::ostream& operator<<(std::ostream& os, const BaseManip& bm)
{
    bm.apply(os);
    return os;
}
// convience function, so we can do std::cout << base(16) << 100;
BaseManip base(int b)
{
    return BaseManip(b);
}
// A custom number output facet.  These are used by the std::locale code in
// streams.  The num_put facet handles the output of numberic values as characters
// in the stream.  Here we create one that knows about our custom manipulator.
struct BaseNumPut : std::num_put<char>
{
    // These absVal functions are needed as std::abs doesnt support 
    // unsigned types, but the templated doPutHelper works on signed and
    // unsigned types.
    unsigned long int absVal(unsigned long int a) const
    {
        return a;
    }
    unsigned long long int absVal(unsigned long long int a) const
    {
        return a;
    }
    template <class NumType>
    NumType absVal(NumType a) const
    {
        return std::abs(a);
    }
    template <class NumType>
    iter_type doPutHelper(iter_type out, std::ios_base& str, char_type fill, NumType val) const
    {
        // Read the value stored in our xalloc location.
        const int base = str.iword(BaseManip::getIWord());
        // we only want this manipulator to affect the next numeric value, so
        // reset its value.
        str.iword(BaseManip::getIWord()) = 0;
        // normal number output, use the built in putter.
        if (base == 0 || base == 10)
        {
            return std::num_put<char>::do_put(out, str, fill, val);
        }
        // We want to conver the base, so do it and output.
        // Base conversion code lifted from Nawaz's answer
        int digits[CHAR_BIT * sizeof(NumType)];
        int i = 0;
        NumType tempVal = absVal(val);
        while (tempVal != 0)
        {
            digits[i++] = tempVal % base;
            tempVal /= base;
        }
        // Get the format flags.
        const std::ios_base::fmtflags flags = str.flags();
        // Add the padding if needs by (i.e. they have used std::setw).
        // Only applies if we are right aligned, or none specified.
        if (flags & std::ios_base::right || 
            !(flags & std::ios_base::internal || flags & std::ios_base::left))
        {
            std::fill_n(out, str.width() - i, fill);
        }
        if (val < 0)
        {
            *out++ = '-';
        }
        // Handle the internal adjustment flag.
        if (flags & std::ios_base::internal)
        {
            std::fill_n(out, str.width() - i, fill);
        }
        char digitCharLc[] = "0123456789abcdefghijklmnopqrstuvwxyz";
        char digitCharUc[] = "0123456789ABCDEFGHIJKLMNOPQRSTUVWXYZ";
        const char *digitChar = (str.flags() & std::ios_base::uppercase)
            ? digitCharUc
            : digitCharLc;
        while (i)
        {
            // out is an iterator that accepts characters
            *out++ = digitChar[digits[--i]];
        }
        // Add the padding if needs by (i.e. they have used std::setw).
        // Only applies if we are left aligned.
        if (str.flags() & std::ios_base::left)
        {
            std::fill_n(out, str.width() - i, fill);
        }
        // clear the width
        str.width(0);
        return out;
    }
    // Overrides for the virtual do_put member functions.
    iter_type do_put(iter_type out, std::ios_base& str, char_type fill, long val) const
    {
        return doPutHelper(out, str, fill, val);
    }
    iter_type do_put(iter_type out, std::ios_base& str, char_type fill, unsigned long val) const
    {
        return doPutHelper(out, str, fill, val);
    }
};
} // namespace StreamManip
int main()
{
    // Create a local the uses our custom num_put
    std::locale myLocale(std::locale(), new StreamManip::BaseNumPut());
    // Set our locacle to the global one used by default in all streams created 
    // from here on in.  Any streams created in this app will now support the
    // StreamManip::base modifier.
    std::locale::global(myLocale);
    // imbue std::cout, so it uses are custom local.
    std::cout.imbue(myLocale);
    std::cerr.imbue(myLocale);
    // Output some stuff.
    std::cout << std::setw(50) << StreamManip::base(2) << std::internal << -255 << std::endl;
    std::cout << StreamManip::base(4) << 255 << std::endl;
    std::cout << StreamManip::base(8) << 255 << std::endl;
    std::cout << StreamManip::base(10) << 255 << std::endl;
    std::cout << std::uppercase << StreamManip::base(16) << 255 << std::endl;
    return 0;
}

自定义操纵器确实是可能的。例如,请参阅此问题。我不熟悉任何一个关于普适基的具体例子。

您确实有两个独立的问题。我想你问的这个问题是完全可以解决的。不幸的是,另一种情况则不那么严重

分配和使用流中的一些空间来保持某些流状态是一个可以预见的问题。流有几个成员(xallociwordpword),它们允许您在流中的数组中分配一个点,并在那里读取/写入数据。因此,流操纵器本身是完全可能的。基本上,您可以使用xalloc在流的数组中分配一个位置来保存当前基数,以便插入运算符在转换数字时使用。

我看不到解决方案的问题更简单:标准库已经提供了一个operator<<来将int插入流中,而且它显然不知道你的假设数据来保存转换的基础。您不能重载它,因为它需要与现有签名完全相同的签名,所以您的重载将是不明确的。

然而,intshort等的重载是重载的成员函数。我猜如果你非常想的话,你可以使用一个模板来重载operator<<。如果我没有记错的话,这将比库提供的与非模板函数的精确匹配更可取。你仍然会违反规则,但如果你把这样一个模板放在命名空间std中,它至少有一定的可能性会起作用。

我试图编写一段代码,它的工作有一些限制。正如其他人(尤其是@Jerry)所指出的那样,它并不是流操纵器,因为这根本不可能。

这是我的代码:

struct base
{
   mutable std::ostream *_out;
   int _value;
   base(int value=10) : _value(value) {}
   template<typename T>
   const base& operator << (const T & data) const
   {
        *_out << data;
        return *this;
   }
   const base& operator << (const int & data) const
   {
        switch(_value)
        {
            case 2:  
            case 4:  
            case 8:  return print(data);
            case 16: *_out << std::hex << data; break;
            default:  *_out << data; 
        }
        return *this;
   }
   const base & print(int data) const
   {
        int digits[CHAR_BIT * sizeof(int)], i = 0;
        while(data)
        {
             digits[i++] = data % _value;  
             data /= _value;
        }
        while(i) *_out << digits[--i] ;
        return *this;
   }
   friend const base& operator <<(std::ostream& out, const base& b)   
   {
       b._out = &out;
       return b;
   }
};

这是测试代码:

int main() {
   std::cout << base(2) << 255 <<", " << 54 << ", " << 20<< "n";
   std::cout << base(4) << 255 <<", " << 54 << ", " << 20<< "n";
   std::cout << base(8) << 255 <<", " << 54 << ", " << 20<< "n";
   std::cout << base(16) << 255 <<", " << 54 << ", " << 20<< "n";
}

输出:

11111111, 110110, 10100
3333, 312, 110
377, 66, 24
ff, 36, 14

在线演示:http://www.ideone.com/BWhW5

限制:

  • 基数不能更改两次。所以这将是一个错误:

    std::cout << base(4) << 879 << base(8) << 9878 ; //error
    
  • 使用base后不能使用其他机械手:

    std::cout << base(4) << 879 << std::hex << 9878 ; //error
    std::cout << std::hex << 879 << base(8) << 9878 ; //ok
    
  • 使用base后不能使用std::endl

    std::cout << base(4) << 879 << std::endl ; //error
    //that is why I used "n" in the test code.
    

我不认为这种语法适用于任意流(使用操纵器,@gigantt链接了一个显示一些替代非操纵器解决方案的答案)。标准操纵器仅设置在流内部实现的选项。

OTOH,你当然可以让这个语法工作:

std::cout << base(4, 20);

其中base是提供流插入运算符的对象(不需要返回临时string)。

相关文章: