首页
学习
活动
专区
圈层
工具
发布
社区首页 >问答首页 >C++字符串格式化程序

C++字符串格式化程序
EN

Code Review用户
提问于 2018-02-10 00:29:18
回答 3查看 1K关注 0票数 12

我有一个平安无事的下午,所以我想我可以尝试写一个字符串格式化程序。

这是基于我找到的这里文档。

代码语言:javascript
复制
#include <string>
#include <vector>
#include <map>
#include <ostream>
#include <iostream>
#include <exception>
#include <stdexcept>
#include <typeindex>
#include <cstdint>
#include <cstddef>
#include <cassert>
#include <iomanip>



template<typename... Args>
class Format
{
    class Formatter
    {
        std::size_t             used;
        // Flags
        enum class Length    {none, hh, h, l, ll, j, z, t, L};
        enum class Specifier {d, i, u, o, x, X, f, F, e, E, g, G, a, A, c, s, p, n};
        bool                    leftJustify;    // -    Left-justify within the given field width; Right justification is the default (see width sub-specifier).
        bool                    forceSign;      // +    Forces to preceed the result with a plus or minus sign (+ or -) even for positive numbers. By default, only negative numbers are preceded with a - sign.
        bool                    forceSignWidth; // (space)  If no sign is going to be written, a blank space is inserted before the value.
        bool                    prefixType;     // #    Used with o, x or X specifiers the value is preceeded with 0, 0x or 0X respectively for values different than zero.
                                                // Used with a, A, e, E, f, F, g or G it forces the written output to contain a decimal point even if no more digits follow. By default, if no digits follow, no decimal point is written.
        bool                    leftPad;        // 0    Left-pads the number with zeroes (0) instead of spaces when padding is specified (see width sub-specifier).
        int                     width;
        int                     precision;
        Length                  length;
        Specifier               specifier;
        std::type_info const*   expectedType;
        std::ios_base::fmtflags format;

        public:
            struct FormatterCheck
            {
                std::ostream&       stream;
                Formatter const&    formatter;
                FormatterCheck(std::ostream& s, Formatter const& formatter)
                    : stream(s)
                    , formatter(formatter)
                {}
                template<typename A>
                std::ostream& operator<<(A const& nextArg)
                {
                    formatter.apply(stream, nextArg);
                    return stream;
                }
            };
           Formatter(char const* formatStr)
                : used(0)
                , leftJustify(false)
                , forceSign(false)
                , forceSignWidth(false)
                , prefixType(false)
                , leftPad(false)
                , width(0)
                , precision(6)
                , length(Length::none)
                , format(0)
            {
                char const* fmt = formatStr;
                assert(*fmt == '%');

                bool flag = true;
                do {
                    ++fmt;
                    switch(*fmt) {
                        case '-':   leftJustify     = true;break;
                        case '+':   forceSign       = true;break;
                        case ' ':   forceSignWidth  = true;break;
                        case '#':   prefixType      = true;break;
                        case '0':   leftPad         = true;break;
                        default:    flag = false;
                    }
                } while (flag);
                if (std::isdigit(*fmt)) {
                    char* end;
                    width = std::strtol(fmt, &end, 10);
                    fmt = end;
                }
                if (*fmt == '.') {
                    ++fmt;
                    if (std::isdigit(*fmt)) {
                        char* end;
                        precision = std::strtol(fmt, &end, 10);
                        fmt = end;
                    }
                    else {
                        precision = 0;
                    }
                }
                char first = *fmt;
                ++fmt;
                switch(first) {
                    case 'h':   length = Length::h;
                                if (*fmt == 'h') {
                                    ++fmt;
                                    length  = Length::hh;
                                }
                                break;
                    case 'l':   length = Length::l;
                                if (*fmt == 'l') {
                                    ++fmt;
                                    length  = Length::ll;
                                }
                                break;
                    case 'j':   length = Length::j;break;
                    case 'z':   length = Length::z;break;
                    case 't':   length = Length::t;break;
                    case 'L':   length = Length::L;break;
                    default:
                        --fmt;
                }
                switch(*fmt) {
                    case 'd':   specifier = Specifier::d;break;
                    case 'i':   specifier = Specifier::i;break;
                    case 'u':   specifier = Specifier::u;break;
                    case 'o':   specifier = Specifier::o;break;
                    case 'x':   specifier = Specifier::x;break;
                    case 'X':   specifier = Specifier::X;break;
                    case 'f':   specifier = Specifier::f;break;
                    case 'F':   specifier = Specifier::F;break;
                    case 'e':   specifier = Specifier::e;break;
                    case 'E':   specifier = Specifier::E;break;
                    case 'g':   specifier = Specifier::g;break;
                    case 'G':   specifier = Specifier::G;break;
                    case 'a':   specifier = Specifier::a;break;
                    case 'A':   specifier = Specifier::A;break;
                    case 'c':   specifier = Specifier::c;break;
                    case 's':   specifier = Specifier::s;break;
                    case 'p':   specifier = Specifier::p;break;
                    case 'n':   specifier = Specifier::n;break;
                    default:
                       throw std::invalid_argument(std::string("Invalid Parameter specifier: ") + *fmt);
                }
                ++fmt;

                expectedType = getType(specifier, length);

                used  = fmt - formatStr;

                format  |= (leftJustify ? std::ios_base::left : std::ios_base::right);

                if (specifier == Specifier::d || specifier == Specifier::i) {
                    format  |= std::ios_base::dec;
                }
                else if (specifier == Specifier::o) {
                    format  |= std::ios_base::oct;
                }
                else if (specifier == Specifier::x || specifier == Specifier::X) {
                    format  |= std::ios_base::hex;
                }
                else if (specifier == Specifier::f || specifier == Specifier::F) {
                    format |= std::ios_base::fixed;
                }
                else if (specifier == Specifier::e || specifier == Specifier::E) {
                    format |= std::ios_base::scientific;
                }
                else if (specifier == Specifier::a || specifier == Specifier::A) {
                    format |= (std::ios_base::fixed | std::ios_base::scientific);
                }
                if (specifier == Specifier::X || specifier == Specifier::F || specifier == Specifier::E || specifier == Specifier::A || specifier == Specifier::G) {
                    format |= std::ios_base::uppercase;
                }
                if (prefixType && (specifier == Specifier::o || specifier == Specifier::x || specifier == Specifier::X)) {
                    format |= std::ios_base::showbase;
                }
                if (prefixType && (specifier == Specifier::a || specifier == Specifier::A || specifier == Specifier::e || specifier == Specifier::E || specifier == Specifier::f || specifier == Specifier::F || specifier == Specifier::g || specifier == Specifier::G)) {
                    format |= std::ios_base::showpoint;
                }
                if (forceSign && (specifier != Specifier::c && specifier != Specifier::s && specifier != Specifier::p)) {
                    format |= std::ios_base::showpos;
                }
            }
            std::size_t size() const {return used;}

            friend FormatterCheck operator<<(std::ostream& s, Formatter const& formatter)
            {
                return FormatterCheck(s, formatter);
            }
            private:
                template<typename A>
                void apply(std::ostream& s, A const& arg) const
                {
                    if (std::type_index(*expectedType) != std::type_index(typeid(A))) {
                        throw std::invalid_argument(std::string("Actual argument does not match supplied argument: Expected(") + expectedType->name() + ") Got(" + typeid(A).name() + ")");
                    }


                    char fill      = (!leftJustify && leftPad && (specifier != Specifier::c && specifier != Specifier::s && specifier != Specifier::p)) ? '0' : ' ';
                    int  fillWidth = width;

                    if (forceSignWidth && !forceSign && (arg >= 0) && (specifier != Specifier::c && specifier != Specifier::s && specifier != Specifier::p)) {
                        s << ' ';
                        --fillWidth;
                    }
                    std::ios_base::fmtflags oldFlags = s.flags(format);
                    char                    oldFill  = s.fill(fill);
                    int                     oldWidth = s.width(fillWidth);
                    std::streamsize         oldPrec  = s.precision(precision);

                    s << arg;

                    s.precision(oldPrec);
                    s.width(oldWidth);
                    s.fill(oldFill);
                    s.flags(oldFlags);
                }

                static std::type_info const* getType(Specifier specifier, Length length)
                {
                    static std::map<std::pair<Specifier, Length>, std::type_info const*>    typeMap = {
                        {{Specifier::d, Length::none}, &typeid(int)},           {{Specifier::i, Length::none}, &typeid(int)},           {{Specifier::d, Length::none}, &typeid(int*)},
                        {{Specifier::d, Length::hh},   &typeid(signed char)},   {{Specifier::i, Length::none}, &typeid(signed char)},   {{Specifier::d, Length::hh},   &typeid(signed char*)},
                        {{Specifier::d, Length::h},    &typeid(short int)},     {{Specifier::i, Length::none}, &typeid(short int)},     {{Specifier::d, Length::h},    &typeid(short int*)},
                        {{Specifier::d, Length::l},    &typeid(long int)},      {{Specifier::i, Length::none}, &typeid(long int)},      {{Specifier::d, Length::l},    &typeid(long int*)},
                        {{Specifier::d, Length::ll},   &typeid(long long int)}, {{Specifier::i, Length::none}, &typeid(long long int)}, {{Specifier::d, Length::ll},   &typeid(long long int*)},
                        {{Specifier::d, Length::j},    &typeid(std::intmax_t)}, {{Specifier::i, Length::none}, &typeid(std::intmax_t)}, {{Specifier::d, Length::j},    &typeid(std::intmax_t*)},
                        {{Specifier::d, Length::z},    &typeid(std::size_t)},   {{Specifier::i, Length::none}, &typeid(std::size_t)},   {{Specifier::d, Length::z},    &typeid(std::size_t*)},
                        {{Specifier::d, Length::t},    &typeid(std::ptrdiff_t)},{{Specifier::i, Length::none}, &typeid(std::ptrdiff_t)},{{Specifier::d, Length::t},    &typeid(std::ptrdiff_t*)},


                        {{Specifier::u, Length::none}, &typeid(unsigned int)},          {{Specifier::o, Length::none}, &typeid(unsigned int)},          {{Specifier::x, Length::none}, &typeid(unsigned int)},          {{Specifier::X, Length::none}, &typeid(unsigned int)},
                        {{Specifier::u, Length::hh},   &typeid(unsigned char)},         {{Specifier::o, Length::hh},   &typeid(unsigned char)},         {{Specifier::x, Length::hh},   &typeid(unsigned char)},         {{Specifier::X, Length::hh},   &typeid(unsigned char)},
                        {{Specifier::u, Length::h},    &typeid(unsigned short int)},    {{Specifier::o, Length::h},    &typeid(unsigned short int)},    {{Specifier::x, Length::h},    &typeid(unsigned short int)},    {{Specifier::X, Length::h},    &typeid(unsigned short int)},
                        {{Specifier::u, Length::l},    &typeid(unsigned long int)},     {{Specifier::o, Length::l},    &typeid(unsigned long int)},     {{Specifier::x, Length::l},    &typeid(unsigned long int)},     {{Specifier::X, Length::l},    &typeid(unsigned long int)},
                        {{Specifier::u, Length::ll},   &typeid(unsigned long long int)},{{Specifier::o, Length::ll},   &typeid(unsigned long long int)},{{Specifier::x, Length::ll},   &typeid(unsigned long long int)},{{Specifier::X, Length::ll},   &typeid(unsigned long long int)},
                        {{Specifier::u, Length::j},    &typeid(std::uintmax_t)},        {{Specifier::o, Length::j},    &typeid(std::uintmax_t)},        {{Specifier::x, Length::j},    &typeid(std::uintmax_t)},        {{Specifier::X, Length::j},    &typeid(std::uintmax_t)},
                        {{Specifier::u, Length::z},    &typeid(std::size_t)},           {{Specifier::o, Length::z},    &typeid(std::size_t)},           {{Specifier::x, Length::z},    &typeid(std::size_t)},           {{Specifier::X, Length::z},    &typeid(std::size_t)},
                        {{Specifier::u, Length::t},    &typeid(ptrdiff_t)},             {{Specifier::o, Length::t},    &typeid(ptrdiff_t)},             {{Specifier::x, Length::t},    &typeid(ptrdiff_t)},             {{Specifier::X, Length::t},    &typeid(ptrdiff_t)},

                        {{Specifier::f, Length::none}, &typeid(double)}, {{Specifier::F, Length::none}, &typeid(double)},   {{Specifier::f, Length::L}, &typeid(long double)}, {{Specifier::F, Length::L}, &typeid(long double)},
                        {{Specifier::e, Length::none}, &typeid(double)}, {{Specifier::E, Length::none}, &typeid(double)},   {{Specifier::e, Length::L}, &typeid(long double)}, {{Specifier::E, Length::L}, &typeid(long double)},
                        {{Specifier::g, Length::none}, &typeid(double)}, {{Specifier::G, Length::none}, &typeid(double)},   {{Specifier::g, Length::L}, &typeid(long double)}, {{Specifier::G, Length::L}, &typeid(long double)},
                        {{Specifier::a, Length::none}, &typeid(double)}, {{Specifier::A, Length::none}, &typeid(double)},   {{Specifier::a, Length::L}, &typeid(long double)}, {{Specifier::A, Length::L}, &typeid(long double)},

                        {{Specifier::c, Length::none}, &typeid(int)},   {{Specifier::c, Length::l}, &typeid(std::wint_t)},
                        {{Specifier::s, Length::none}, &typeid(char*)}, {{Specifier::c, Length::l}, &typeid(wchar_t*)},

                        {{Specifier::p, Length::none}, &typeid(void*)}
                    };
                    auto find = typeMap.find({specifier, length});
                    if (find == typeMap.end()) {
                        throw std::invalid_argument("Specifier and length are not a valid combination");
                    }
                    return find->second;
                }
    };
    std::string                     format;
    std::tuple<Args const&...>      arguments;
    std::vector<std::string>        prefixString;
    std::vector<Formatter>          formater;
    public:
        Format(char const* fmt, Args const&... args)
            : format(fmt)
            , arguments(args...)
        {
            std::size_t count = sizeof...(args);
            std::size_t pos   = 0;
            for(int loop = 0; loop < count; ++loop) {
                // Not dealing with '\%' yet just trying to get it working.
                std::size_t nextFormatter = format.find('%', pos);
                if (nextFormatter == std::string::npos) {
                    throw std::invalid_argument("Invalid Format: not enough format specifiers for provided arguments");
                }
                prefixString.emplace_back(format.substr(pos, (nextFormatter - pos)));

                formater.emplace_back(format.data() + nextFormatter);
                pos = nextFormatter + formater.back().size();
            }
            std::size_t nextFormatter = format.find(pos, '%');
            if (nextFormatter != std::string::npos) {
                throw std::invalid_argument("Invalid Format: too many format specifiers for provided arguments");
            }
            prefixString.emplace_back(format.substr(pos, format.size() - pos));
        }
        void print(std::ostream& s) const
        {
            doPrint(s, std::make_index_sequence<sizeof...(Args)>());
        }
    private:
        template<typename A>
        struct Printer
        {
            Printer(std::ostream& s, std::string const& prefix, Formatter const& format, A const& value)
            {
                s << prefix << format << value;
            }
        };
        template<typename A>
        void forward(A const&...) const{}
        template<std::size_t... I>
        void doPrint(std::ostream& s, std::index_sequence<I...> const&) const
        {
            forward(1, Printer<decltype(std::get<I>(arguments))>(s, prefixString[I], formater[I], std::get<I>(arguments))...);
            s << prefixString.back();
        }

        friend std::ostream& operator<<(std::ostream& s, Format const& format)
        {
            format.print(s);
            return s;
        }
};

template<typename... Args>
Format<Args...> make_format(char const* fmt, Args const&... args)
{
    return Format<Args...>(fmt, args...);
}

测试Harness

代码语言:javascript
复制
int main()
{
    std::cout << make_format("Test One\n");
    std::cout << make_format("Test Two   %d\n", 12);
    std::cout << make_format("Test Three %d %f\n", 12, 4.56);
    std::cout << make_format("Test Four  %d %d\n", 12, 4.56); // Should throw
}

目前它并不是很有效率。除了代码审查之外,最好能了解如何使其更高效。也许我们可以在编译时而不是运行时解析字符串。

创建格式化程序对象时。它在字符串中找到'%‘格式规范。然后确保与参数相同数量的格式说明符。

当您打印"Format“对象时,它基本上强制对每个参数调用Formatter::apply()。这将在打印参数之前设置适当的标志,然后将其设置为原始值。格式化程序对象是在构造Format期间创建的,但在将对象发送到流之前不会应用。

EN

回答 3

Code Review用户

发布于 2018-02-10 13:53:20

类型/可移植性问题:

这可能是一个我不知道的特性,但它没有使用Visual 2015进行编译,因为A不是一个参数包:

代码语言:javascript
复制
template<typename A>
void forward(A const&...) const {}

可能应该是:

代码语言:javascript
复制
template<typename... A>
void forward(A const&...) const {}

(BUG:)这两个论点是错误的!

代码语言:javascript
复制
std::size_t nextFormatter = format.find(pos, '%');

MSVC通过conversion from 'size_t' to 'char', possible loss of data警告捕捉到这一点。

在MSVC上产生警告/错误的其他小问题:

代码语言:javascript
复制
for (int loop = 0; loop < count; ++loop) { // signed / unsigned mismatch
...
int oldWidth = s.width(fillWidth); // conversion from 'std::streamsize' to 'int', possible loss of data

auto只在一个地方使用来保存类型,但它也可以极大地帮助防止像这样的意外转换,因为它迫使您只在必要时才考虑类型。(也就是说,上面的for循环要求您考虑类型,因为您没有从现有变量中获取类型。宽度声明不需要任何考虑:您需要函数返回值的类型)。

std::isdigit需要一个#include <cctype>

命名:

考虑使用枚举和更多的描述性名称,而不是布尔语和注释。例如:

代码语言:javascript
复制
enum JustificationType { Left, Right };
JustificationType justification;

enum SignDisplayType { Always, OnlyPositive };
SignDisplayType signDisplay;

enum SignPaddingType { PadWhenNoSign, DontPad };
SignPaddingType signPadding;

这会将选项的文档直接放在代码中。

票数 6
EN

Code Review用户

发布于 2018-02-10 07:04:01

我们可以简化参数包,解压缩如下:

代码语言:javascript
复制
private:
    // The template parameter is the index of the argument we are printing.
    // Generated from the doPrint() function.
    template<std::size_t I>
    std::ostream& printValue(std::ostream& s) const
    {   
        return s << prefixString[I] << formater[I] << std::get<I>(arguments);
    }   

    // Called from print()
    //    The std::index_sequence<> is constructed based on the number of
    //    arguments passed to the constructor. This mean I is a sequence
    //    of numbers from 0 ... A   (A is one less the number of param)
    //    We can use this to call printValue() A times using parameter
    //    pack expansion.
    template<std::size_t... I>
    void doPrint(std::ostream& s, std::index_sequence<I...> const&) const
    {   
        std::ostream* ignore[] = {&printValue<I>(s)...};
        s << prefixString.back();
    }   

这就消除了“减少美国”的功能:

代码语言:javascript
复制
    template<typename A>
    void forward(A const&...) const{}

我们将struct Printer转换为函数调用printValue()

代码语言:javascript
复制
    template<typename A>
    std::ostream& printValue(std::ostream&      s,
                             std::string const& prefix,
                             Formatter const&   format,
                             A const&           value) const

这意味着编译器可以进行参数类型推断。因此,呼叫可以简化如下:

代码语言:javascript
复制
 // from 
 Printer<decltype(std::get<I>(arguments))>(s, prefixString[I], formater[I], std::get<I>(arguments))...

 // too
 printValue(s, prefixString[I], formater[I], std::get<I>(arguments))...

但是现在它是一个函数(而不是一个类),它可以访问成员。所以我们实际上不需要传递所有这些参数。所以我们可以简化得更多:

代码语言:javascript
复制
 // too
 template<std::size_t I>
 std::ostream& printValue(std::ostream& s) const

 // Now the call is simply
 printValue<I>(s)...
票数 4
EN

Code Review用户

发布于 2018-02-10 19:40:19

代码评审

代码看起来很大,很吓人,也很难接近。我并没有深入研究它,但以下是我所看到的一瞥:

  • 模板似乎没有得到充分利用。将参数与操作关联的主要机制似乎是运行时type_info。在不同的说明符上拆分、应用和使用std::is_same_v<T, U>是很好的。
  • 当需要函数时,这个函数很容易通过更改一些名称来修复。
  • 不要存储延长生命的引用而不禁止构造函数使用参数,例如: Format format(“the”,"a","b");std::cout <<格式;//里面的字符串现在隐藏中间步骤,例如,使它成为一个函数,并在一些详细名称空间中隐藏类。

替代方法

以下是我对这种格式的偏爱:

代码语言:javascript
复制
{x} -> here stands xth argument
other specifiers are inside {}, e.g. {x:4} -> fit in 4 chars

它可能会很慢(虽然不是不合理的),但它的方便将很容易地平衡这一点。以下是我认为上述各点较佳的原因:

  • 不需要重复类型
  • 不需要多次传递一个参数就可以在输出中复制它,只需将{x}放在其中。
  • 看上去不到90's (甚至可能是80's)

主要问题是在运行时获取第n个参数。很久以前,牦牛给了我一个素描。从那时起,我的模板元编程技能得到了提高,所以我决定自己编写。

代码语言:javascript
复制
template <std::size_t Index, typename Tuple>
void fallthrough(const Tuple& tup, std::ostream& os, std::size_t runtime_index)
{
    if (runtime_index == Index)
    {
        os << std::get<Index>(tup);
        return;
    }

    if constexpr (Index != 0)
    {
        return fallthrough<Index - 1>(tup, os, runtime_index);
    }

    throw "unreachable";
}

其主要思想是继续下降,直到达到运行时索引为止。Fallthrough演示。但有趣的是,如果在编译时就知道索引,那么整个过程就会消失。此外,如果类型相同,它将衰变为跳转表。我确信编译器即使在类型不一样的情况下也可以生成跳转表,但我猜它知道一些我不知道的事情,反之亦然。

现在,在我们的掌握中,我们可以编写格式化程序:

代码语言:javascript
复制
#include <regex>
#include <cstddef>
#include <iosfwd>
#include <utility>
#include <string_view>
#include <string>

namespace details
{
    template <typename InputIterator>
    std::size_t to_size(InputIterator first, InputIterator last)
    {
        std::size_t size{0};

        for (; first != last; ++first)
        {
            size += size * 10 + *first - '0';
        }

        return size;
    }

    template <std::size_t Index, typename Tuple>
    void fallthrough(const Tuple& tup, std::ostream& os, std::size_t runtime_index)
    {
        if (runtime_index == Index)
        {
            os << std::get<Index>(tup);
            return;
        }

        if constexpr (Index != 0)
        {
            return fallthrough<Index - 1>(tup, os, runtime_index);
        }

        throw "unreachable";
    }
}

namespace nice_formatter
{
    template <typename ... Args>
    void format_nicely(std::ostream& os, const std::string& format, const Args& ... args)
    {
        auto tied = std::tie(args...);

        std::regex arg_regex{"\\{\\d+\\}"};
        std::sregex_iterator first_match{format.begin(), format.end(), arg_regex};
        std::sregex_iterator last_match{};
        auto last_pos = format.c_str();

        while (first_match != last_match)
        {
            //output the stuff preceeding it
            size_t match_index = first_match->position();
            os << std::string_view{last_pos, static_cast<std::size_t>(&format[match_index] - last_pos)};
            last_pos = &format[match_index] + first_match->length();


            const char* argindex_str = &format[match_index];
            //discard the { and } when passing to to_size
            auto arg_index{details::to_size(argindex_str + 1, argindex_str + first_match->length() - 1)};
            if (arg_index > sizeof...(Args))
            {
                throw std::invalid_argument{"Not enough arguments provided"};
            }
            details::fallthrough<sizeof...(Args) - 1>(tied,
                                                      os,
                                                      arg_index);

            ++first_match;
        }
    }
}

(它里面可能有很多bug,我还没有对它进行太多的测试)

可以通过更改regex模式并在循环中添加更多的逻辑来添加说明符。

很小的例子:

代码语言:javascript
复制
#include <iostream>

int main()
{
    std::size_t x = 0;
    std::string some_text = "some text";
    double y = 1.2;

    nice_formatter::format_nicely(std::cout, "{0} {1} {2}\n", x, some_text, y);
    nice_formatter::format_nicely(std::cout, "Duplicate of \"{0}\" is \"{0}\"", some_text);
}

更多的想法

格式字符串通常是原始字符串文本,因此它是一个常量表达式。。也许可以编写一些类,以便能够在编译时标记格式字符串。

注意:请在本地机器上运行上述代码。对于编译器来说,这是非常复杂的,因为即使在上面的例子中,也需要花费大约一秒钟的时间。尝试在在线编译器上运行它可能需要相当长的时间。

票数 4
EN
页面原文内容由Code Review提供。腾讯云小微IT领域专用引擎提供翻译支持
原文链接:

https://codereview.stackexchange.com/questions/187230

复制
相关文章

相似问题

领券
问题归档专栏文章快讯文章归档关键词归档开发者手册归档开发者手册 Section 归档