未知参数处理

可变参数

main.cpp
#include <iostream>

#include "mclog.h"
#include "replace_args.h"

int main(int argc, char **argv)
{
    // 使用 const 保持数据不变
    const std::string name = "JudyHopps";
    const std::string introduce = "我是兔朱迪和尼克狐尼克组成了动物城最强搭档";
    const std::string content = "\n这里是兔子胡萝卜节表演大会\n请 {1} 进行自我介绍\n{2}\n";

    MCLOG("使用 replace_str_ref 函数");
    std::string tm_replace_str_ref = content;
    replace_str_ref(tm_replace_str_ref, "{1}", name);
    replace_str_ref(tm_replace_str_ref, "{2}", introduce);
    MCLOG($(tm_replace_str_ref));

    MCLOG("\n使用 replace_args 对象,多次替换");
    std::string tm_replace_args = content;
    std::string ret_replace_args = replace_args(tm_replace_args)("{1}", name, "{2}", introduce);
    MCLOG($(ret_replace_args));

    MCLOG("\n使用 replace_args 对象,只替换一次");
    std::string tm_replace_args_1 = content;
    std::string ret_replace_args_1 = replace_args(tm_replace_args_1)("{1}", name);
    MCLOG($(ret_replace_args_1));

    return 0;
}
replace_args.h
#ifndef REPLACE_ARGS_H
#define REPLACE_ARGS_H

#include <iostream>
#include "format.h"

// 可以接收可变数据的字符串替换格式化类
struct replace_args
{
    // 通过构造哈数,赋值一份字符串到 _org 变量
    replace_args(const std::string &org) : _org(org) {}

    // replace 无参数版本
    // 处理完成之后,最终的数据在 _org 中,返回结果
    // 这个函数只调用一次
    std::string replace()
    {
        return _org;
    }

    // replace 有参数版本
    // 可变参数模板函数 Targ 代表数量未知的
    // str_old str_new 这两个参数会从 arg 这个未知数量上拿走两个数据 arg 的数量 -2
    // 这个函数可能调用多次
    template <typename... Targ>
    std::string replace(const std::string &str_old, const std::string &str_new, Targ... arg)
    {
        // 调用 format.h replace_str_ref 来处理数据
        replace_str_ref(_org, str_old, str_new);

        // 递归调用
        // 如果 arg 的数量大于0,调用 replace 有参数版本
        // arg 如果这一次数量为0,调用 replace 无参数版本
        return replace(arg...);
    }

    // 重载 () 括号操作符,自定义返回 std::string 类型
    // 并调用 replace 函数,根据 arg 的数量选择不同版本
    // 这个函数只调用一次
    template <typename... Targ>
    std::string operator()(Targ... arg)
    {
        return replace(arg...);
    }

    // 储存传入数据的复制版本
    std::string _org;
};

#endif // REPLACE_ARGS_H
打印结果
使用 replace_str_ref 函数 [/home/red/open/github/mcpp/example/10/main.cpp:13]
[tm_replace_str_ref: 
这里是兔子胡萝卜节表演大会
请 JudyHopps 进行自我介绍
我是兔朱迪和尼克狐尼克组成了动物城最强搭档
]  [/home/red/open/github/mcpp/example/10/main.cpp:17]

使用 replace_args 对象,多次替换 [/home/red/open/github/mcpp/example/10/main.cpp:19]
[ret_replace_args: 
这里是兔子胡萝卜节表演大会
请 JudyHopps 进行自我介绍
我是兔朱迪和尼克狐尼克组成了动物城最强搭档
]  [/home/red/open/github/mcpp/example/10/main.cpp:22]

使用 replace_args 对象,只替换一次 [/home/red/open/github/mcpp/example/10/main.cpp:24]
[ret_replace_args_1: 
这里是兔子胡萝卜节表演大会
请 JudyHopps 进行自我介绍
{2}
]  [/home/red/open/github/mcpp/example/10/main.cpp:27]

当了解上一篇文章的面向对象之后,你已经算入门了代码编程,或许你应该需要开始了解一些编程语言的小技巧
今天的带来的技巧是模板可变参数,这是一个很有意思的东西,这个技术本质上是一种可有可无的东西,但是这个技术却可以让你少写很多代码,可以在开发者简化代码的同时让调用者更方便的使用

多次替换

你应该先看看 main.cpp 文件中,两种替换字符串的使用,在使用 replace_str_ref 函数时,你需要替换几次就需要调用这个函数几次,但使用 replace_args 类是,它总是可以通过一行代码就完成数次替换,而且它是可选的,你想替换几次就替换几次
replace_args 实现这种功能的原因是因为它可以接收多个参数,而且参数的数量是可以不确定的,只要符合规则即可

可变参数声明
// 终止版本
std::string replace()
{
    return "END";
}

// 运行版本
template <typename... Targ>
std::string replace(const std::string &one, const std::string &two, Targ... args)
{
    return replace(args...);
}

上面代码判断 replace 函数就是一个可接收多个参数的可变参数声明例子,replace 需要存在至少两个版本,一个运行版本,一个终止版本,replace 函数总是递归调用的
当满足这些要求,你还差一步就是声明 Targ… 类型,获取到 args 变量,请注意 typename… Targ 与 Targ… 和 args… 这三个 … 的顺序和位置,这是必须的,你问我为什么怎么乱我也不知道,反正规定就这样,毕竟C++这种糟糕的规定也不是一天两天了
当你有了递归函数,有了 Targ… 可变数量类型,有了 replace 的运行和终止版本这三个前置条件,那现在就可以获取可变数量的参数和类型了,然后你只需要在 replace 函数的运行版本上指定你需要取什么数据就可以
注意这个 replace 运行版本的写法,Targ… args 必须放在最后,如何我要取2个变量,就定义 one two
,三个呢 one two three 你随意就好,这只是表明你希望一次性从 args 上获取几个数据,但是 args 一定是参数数量的整倍数才行,否则会报错
值得注意的是,这里的例子 replace 使用的是两个参数,one two 都刚好是 std::string 实际上他们可以是任何类型,整数也好字符也罢,当然名字也是可以变的,只要符合后续运行的代码即可
这意为着可变参数一旦满足三个前置条件,你可以定制任何数据数量参数,定制参数顺序的类型,定制每个变量的名称,定制多个运行版本,反正可变参数模板的编写真的很随意,只要符合规则就可以运行
如果你想更改参数类型,在代码的写法上可以参考 replace_args 类,然后剔除实际执行代码,编译成功之后在加上实际运行代码即可

递归调用

replace_args 类的代码中提到递归调用,其实递归代码非常好理解,一个函数反复的执行自身,直到遇到终止条件,如果没有终于则直接栈溢出报错
需要注意的是,递归执行虽然每一次都执行的同一个函数,但实际上每个函数都是被单独复制出来的,每个函数的内部变量等数据会被放到函数栈中,然后以层层递进的方式执行函数,每一个递归函数的状态都是独立且全新的
虽然在执行递归函数时,看起来同一个函数被执行了很多次,但是在调用者看来就执行了一次,也就是外部调用只看第一次递归函数的执行结果,不关心其他深层递归在干嘛,只不过第一次递归函数的执行结果往往由最后一次执行决定而已
相关递归栈与递归执行返回需要自行学习

重载符号
// 重载符号写法
struct replace_args
{
    // 构造函数
    replace_args(const std::string &org) : _org(org) {}

    // 重载符号
    template <typename... Targ>
    std::string operator()(Targ... arg)
    {
        return replace(arg...);
    }
};

// 临时对象调用
std::string ret_replace_args = replace_args(tm_replace_args)("{1}", name, "{2}", introduce);

// 声明变量的等价调用
replace_args obj(tm_replace_args);
std::string ret_replace_args = obj("{1}", name, "{2}", introduce)

可以看出 replace_args 类对象的调用很奇怪,居然是使用两个小括号来传递参数和调用的,而且一行代码就完成了参数传递和结果返回
这里的 replace_args 语法第一个小括号是 构造函数调用 第二个是 operator() 小括号重载调用
使用小括号重载之后,就可以通过小括号接收传递参数,可以更方便的使用 replace_args 的运行,而已可以从 声明变量的等价调用 看出
创建出来的 obj 对象也可以使用小括号调用,是因为实际上 临时对象调用 replace_args(tm_replace_args) 这个写法是创建了一个临时对象,临时对象可以和正常对象一样去使用类的函数
要记住临时对象是没有名字的,下一行代码就无法引用,所以临时对象只有一行代码的作用域,下一行代码就自动销毁了

使用建议

其实可以从例子中看出可变参数模板也不是非用不可,但是使用它确实可以减少一部分代码的编写,让调用者更方便,但是同时也增加了代码复杂度
所以我建议新手不要轻易使用这类可以简化代码的小技巧,即使使用也要在代码复杂度和调用者便捷之间取得平衡
当然你已经足够熟练,我建议可以多使用类似的技巧让一部分代码看上去更优雅一些
如果你是新手,要注意 replace_str_ref 这个函数是非常低效的,在其他非教学类代码上,请不要使用这个函数
如果你没有 replace_str_ref 函数的实际代码,它出现在 今天是星期几 这篇文章

项目路径

https://github.com/HellowAmy/mcpp.git
Logo

有“AI”的1024 = 2048,欢迎大家加入2048 AI社区

更多推荐