C++编译时将整数转换为字符串

25

我想做类似于这样的事情:

template<int N>
char* foo() {
  // return a compile-time string containing N, equivalent to doing
  // ostringstream ostr; 
  // ostr << N;
  // return ostr.str().c_str();
}

看起来 boost MPL 库可能允许这样做,但我无法真正弄清楚如何使用它来实现这一点。这是有可能的吗?


需要像那样定义一个函数吗?你可以使用预处理器宏更轻松地完成(只会失去类型安全性)。 - vanza
itoa()函数可以解决问题。http://www.cplusplus.com/reference/clibrary/cstdlib/itoa/ - Ram
1
定义一个编译时字符串... std::string 是运行时的。你可以做半运行时的魔法,但不是纯编译时的。预处理器是你最好的选择。 - user405725
1
@Ram:OP希望在编译时进行转换。itoa函数只能在运行时使用。 - Thomas Matthews
澄清一下,得到一个常量字符指针也是可以的。 - Andrey
@vanza,我该如何使用宏来实现这个? - Andrey
6个回答

30
首先,如果通常您在运行时知道数字,那么您可以轻松地构建相同的字符串。也就是说,如果您的程序中有 12,您也可以有 "12"
预处理器宏也可以向参数添加引号,因此您可以编写以下代码:
#define STRINGIFICATOR(X) #X

每当你编写STRINGIFICATOR(2)时,它将产生"2"。

然而,实际上可以使用编译时元编程(compile-time metaprogramming)来完成,而无需使用宏定义。这并不是直接的操作,因此我无法给出确切的代码,但我可以给您一些实现思路:

  1. 使用要转换的数字编写一个递归模板。该模板将递归到基本情况,即数字小于10。
  2. 在每次迭代中,您可以将N%10位的数字转换为字符,如T.E.D.所建议的那样,并使用mpl::string来构建编译时字符串,以附加该字符。
  3. 最终将构建一个mpl::string,其中包含静态value()字符串。

我花了时间将其实现为个人练习。最终效果还不错:

#include <iostream>
#include <boost/mpl/string.hpp>

using namespace boost;

// Recursive case
template <bool b, unsigned N>
struct int_to_string2
{
        typedef typename mpl::push_back<
                typename int_to_string2< N < 10, N/10>::type
                                         , mpl::char_<'0' + N%10>
                                         >::type type;
};

// Base case
template <>
struct int_to_string2<true,0>
{
        typedef mpl::string<> type;
};


template <unsigned N>
struct int_to_string
{
        typedef typename mpl::c_str<typename int_to_string2< N < 10 , N>::type>::type type;
};

int
main (void)
{
        std::cout << int_to_string<1099>::type::value << std::endl;
        return 0;
}

1
很酷,我刚刚在做这个,但不知道如何连接字符 :) - Karoly Horvath
@yi_H:mpl 提供了类型 push_front<string_type, char>::type,用于定义一个在现有字符串前添加字符的编译时字符串。 - Diego Sevilla
@Diego:你的代码似乎没有处理负值。另外,对于那些好奇的人,我之前写过这个代码的反向(使用mpl::string进行编译时字符串到整数的转换);代码可以在这个答案中找到。 - ildjarn
@ildjam:是的,它不支持负数。我只是利用空闲时间练习了一下。据我所知,添加负数支持也不会太困难。 - Diego Sevilla
我很感激这个解决方案,但是哎呀,这么多代码只完成了这么点儿事情。 - Apprentice Queue
显示剩余2条评论

22

我知道这个问题现在已经有几年了,但是我想要一个使用纯C++11的解决方案,没有boost依赖。因此,这里提供一些代码(灵感来自于另一个问题的答案):

```cpp // code goes here ```
```cpp // 代码放在这里 ```
/* IMPLEMENTATION */

/* calculate absolute value */
constexpr int abs_val (int x)
    { return x < 0 ? -x : x; }

/* calculate number of digits needed, including minus sign */
constexpr int num_digits (int x)
    { return x < 0 ? 1 + num_digits (-x) : x < 10 ? 1 : 1 + num_digits (x / 10); }

/* metaprogramming string type: each different string is a unique type */
template<char... args>
struct metastring {
    const char data[sizeof... (args)] = {args...};
};

/* recursive number-printing template, general case (for three or more digits) */
template<int size, int x, char... args>
struct numeric_builder {
    typedef typename numeric_builder<size - 1, x / 10, '0' + abs_val (x) % 10, args...>::type type;
};

/* special case for two digits; minus sign is handled here */
template<int x, char... args>
struct numeric_builder<2, x, args...> {
    typedef metastring<x < 0 ? '-' : '0' + x / 10, '0' + abs_val (x) % 10, args...> type;
};

/* special case for one digit (positive numbers only) */
template<int x, char... args>
struct numeric_builder<1, x, args...> {
    typedef metastring<'0' + x, args...> type;
};

/* convenience wrapper for numeric_builder */
template<int x>
class numeric_string
{
private:
    /* generate a unique string type representing this number */
    typedef typename numeric_builder<num_digits (x), x, '\0'>::type type;

    /* declare a static string of that type (instantiated later at file scope) */
    static constexpr type value {};

public:
    /* returns a pointer to the instantiated string */
    static constexpr const char * get ()
        { return value.data; }
};

/* instantiate numeric_string::value as needed for different numbers */
template<int x>
constexpr typename numeric_string<x>::type numeric_string<x>::value;

/* SAMPLE USAGE */

#include <stdio.h>

/* exponentiate a number, just for fun */
static constexpr int exponent (int x, int e)
    { return e ? x * exponent (x, e - 1) : 1; }

/* test a few sample numbers */
static constexpr const char * five = numeric_string<5>::get ();
static constexpr const char * one_ten = numeric_string<110>::get ();
static constexpr const char * minus_thirty = numeric_string<-30>::get ();

/* works for any constant integer, including constexpr calculations */
static constexpr const char * eight_cubed = numeric_string<exponent (8, 3)>::get ();

int main (void)
{
    printf ("five = %s\n", five);
    printf ("one ten = %s\n", one_ten);
    printf ("minus thirty = %s\n", minus_thirty);
    printf ("eight cubed = %s\n", eight_cubed);

    return 0;
}

输出:

five = 5
one ten = 110
minus thirty = -30
eight cubed = 512

如何从constexpr函数的循环中调用此函数?numeric_builder<i>,我的编译器说参数不是常量表达式? - pixelblender

13

使用C++14即可实现此功能,无需外部依赖。标准的关键扩充是具有非平凡constexpr构造函数的能力,使得该功能可以包含在一个简单的类中。

给定一个整数模板参数,构造函数可以执行整数到字符串的转换。这被存储在一个成员字符缓冲区中,其大小由另一个constexpr函数确定。然后,一个用户定义的转换函数提供对缓冲区的访问:

#include <cstdint>

template<std::intmax_t N>
class to_string_t {

    constexpr static auto buflen() noexcept {
        unsigned int len = N > 0 ? 1 : 2;
        for (auto n = N; n; len++, n /= 10);
        return len;
    }

    char buf[buflen()] = {};

public:
    constexpr to_string_t() noexcept {
        auto ptr = buf + buflen();
        *--ptr = '\0';

        if (N != 0) {
            for (auto n = N; n; n /= 10)
                *--ptr = "0123456789"[(N < 0 ? -1 : 1) * (n % 10)];
            if (N < 0)
                *--ptr = '-';
        } else {
            buf[0] = '0';
        }
    }

    constexpr operator const char *() const { return buf; }
};

最后,一个变量模板(另一个C++14的新增功能)简化了语法:

template<std::intmax_t N>
constexpr to_string_t<N> to_string;

puts(to_string<62017>); // prints "62017"

该功能可以扩展以支持其他进制(例如十六进制),宽字符类型和常见的容器接口;我将所有内容打包到一个头文件中,并在GitHub上发布,网址是:tcsullivan/constexpr-to-string

使用C++20,这也可以扩展以支持浮点数。需要一个容器类型来处理浮点字面量,而此前它不能作为模板参数。请查看GitHub存储库中的f_to_string.hpp头文件以获取实现。


太好了!这很有效。唯一的小问题是能否选择十六进制的大小写。 - Alexis Wilke
由于这个答案在现代C++中更加有用,因此将接受的答案更换为此答案。 - Andrey
1
这实际上是一个链接式答案,指向您在Github上的代码。此答案中没有展示转换代码的实际工作原理或使用了哪些C++17特性(可能是constexpr允许的扩展)。最好至少在答案本身中展示一个最简版本。 - Peter Cordes
1
@PeterCordes 你说得对,这需要一个真正的解释。我已经更新了我的答案,使其更加详尽,并发现 C++14 实现(现在显示)是可能的。 - clyne

7
也许我错过了什么,但这应该很简单:
 #define NUM(x) #x

很遗憾,这不适用于非类型模板参数。


6
然而,这对于任意编译时整数都不起作用。NUM(1+1)会得到"1+1"的结果。 - hammar
2
为了允许其他宏作为参数使用,我建议使用额外的间接方式:#define _NUM(x) #x 后跟 #define NUM(x) _NUM(x) - vanza
5
即使是枚举和已知的编译时常量值也不能正常工作,例如 NUM(foo);,其中 enum { foo = 42 };。宏将产生 "foo" 而不是 "42" - greatwolf

4

在一些情况下,如果你知道数字范围永远不会超出0..9,则可以使用以下技巧:

return '0' + N;

一开始可能有点受限,但是我很惊讶这种情况有多少次出现。

哦,我知道这返回的是一个char而不是std::string。这是一个特性。string不是内置语言类型,因此没有办法在编译时创建它。


2
在这种情况下,整数可以是任何值。 - Andrey
2
这不符合返回 char * 的要求。 - M.M
1
@M.M - 需求分析中被严重低估的一部分是识别什么是真正的需求,而不是基于实现期望的假设。在这种情况下,问题字面上说“像这样做某事”。上述内容确实做了类似的事情。 - T.E.D.

3

另一个有用的选项:

template <int i, bool gTen>
struct UintToStrImpl
{
   UintToStrImpl<i / 10, (i > 99)> c;
   const char c0 = '0' + i % 10;
};

template <int i>
struct UintToStrImpl <i, false> 
{ 
   const char c0 = '0' + i; 
};

template <int i, bool sign>
struct IntToStrImpl
{
   UintToStrImpl<i, (i > 9)> num_;
};

template <int i>
struct IntToStrImpl <i, false>
{
   const char sign = '-';
   UintToStrImpl<-i, (-i > 9)> num_;
};

template <int i>
struct IntToStr
{
   IntToStrImpl<i, (i >= 0)> num_;
   const char end = '\0';
   const char* str = (char*)this;
};

std::cout << IntToStr<-15450>().str;

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