Default Arguments总结
  ZXL8ALkrtBPG 2023年11月02日 38 0
cpp

默认实参

默认实参在C++编程实践中非常常见,但其中也有一些有趣的知识点与扩展,本文对默认实参做简单总结;

函数默认实参

默认实参用来取代函数调用中缺失的尾部实参 :

void Process(int x = 3, int y = 4){}
  1. 拥有默认实参的形参之后的形参必须在同一作用域内有声明提供了默认参数,否则编译失败:
void Process(int x = 3, int y){} // 编译失败
void Process(int x, int y = 4);
void Process(int x = 3, int y){} // 编译成功
void Process(int x, int y = 4);
namespace Test {
void Process(int x = 3, int y){} // 编译失败,不在同一作用域
}
  1. 形参包展开情况:
template<typename ...T>
void Process(int f = 0, T ...) {}
  1. 省略号不是形参,所以它可以跟在带有默认实参的形参之后
void Process(int f = 0, ...) {}

省略号在函数重载决议时拥有最低优先级,可以用于配合模板实参的约束,例如可以用于实现IsConstructible,如下所示:

template<typename U>
class IsConstructible {
private:
template<typename T, typename = std::void_t<decltype(T())>>
static std::true_type Test(void*);

template<typename T, typename = std::void_t<>>
static std::false_type Test(...);
public:
static constexpr bool value = std::is_same_v<std::true_type, decltype(Test<U>(nullptr))>;
};

成员函数默认实参

对于非模板类的成员函数,类外的定义的默认实参与类体内的声明所提供的默认实参相组合

class Test {
public:
    void Process(int a, int b = 4);
};
void Test::Process(int a = 3, int b) 
{
}

对于继承中的成员函数默认参数需要注意,看一下代码示例:

struct Base {
    virtual void Process(int a = 3, int b = 4)
    {
    }
};

struct Derive: Base {
    void Process(int a = 5, int b = 6)
    {
        DBG_LOG("a: %d b: %d", a, b);
    }
};

int main()
{
    Derive d {};
    Base &b = d;
    b.Process();    
}
输出:2023-4-15:10:45:25.148623[DBG  ][Process:19] a: 3 b: 4

可以发现派生类的默认实参并没有覆盖基类,如果直接想当然在实践中就会落下bug,改写下调用:

int main()
{
    Derive d {};
    d.Process();    
}
输出:2023-4-15:10:49:17.761961[DBG  ][Process:19] a: 5 b: 6

直接用派生类调用Process函数,则发现此时默认参数变成了派生类的默认实参,这其中的原因为默认实参是跟对象的静态类型绑定的;因此在实际编码中往往会规范派生类函数不设置默认实参,以免造成不必要的困惑和bug;

此外还有一些约束,可作为了解【1】:

// 默认实参中不能使用局部变量;
// 默认实参中不能使用 [`this`](this.html) 指针: 
class A
{
    void f(A* p = this) {} // 错误:不能使用 this
};
// 默认实参中不能使用非静态的类成员
class Test {
public:
    void Process(int a = m_a) {} // 编译OK
    static inline int m_a = 0;
};
class Test {
public:
    int operator[](int i = 0) { return 0; } // 编译失败
    int operator()(int i = 0) { return 0; } // 编译成功
};

函数模板默认模板实参

template<typename T1, typename T2 = int> // 编译OK
void Process(){}
template<typename T1 = int, typename T2>  // 编译OK
void Process(){}

上述代码中模板实参并不要求是尾部实参,这点与函数实参有区别;

类模板默认模板实参

  1. 对于类模板默认实参和函数默认实参类似,需要具有默认实参之后的形参都具有默认参,可以与类模板声明中的默认实参进行组合:
template<typename T1, typename T2, typename T3 = int>
class Test;

template<typename T1, typename T2 = int, typename T3>
class Test {
};

但是不能对模板参数进行重复的默认实参设置;

  1. 函数模板本身不支持偏特化,但类模板支持,而类模板偏特化中不支持默认实参:
template<typename T1>
class Test {
};

template<typename T1 = int>
class Test<T1*>{
};
// 编译错误:default template arguments may not be used in partial specializations
  1. 对于类模板类外定义的函数不支持默认实参:
template<typename T1>
class Test {
    void Process();
};

template<typename T = int> // 编译失败
void Test<T>::Process()
{
}
  1. 类内友元函数定义支持默认实参
class Test {
    template<typename T = int>
    friend void Process(Test *)
    {}
    // 类内声明,类外定义,则不支持默认实参,编译错误
    // template<typename T = int>
    // friend void Process(Test *); 
private:
    int m_val;
};
  1. 可变参模板不支持默认模板实参

默认实参扩展

经过上述的简单总结后,可以发现对于函数模板、类模板默认模板实参确定后,用户需要顺序指定模板实参:

template<typename T0 = Default, typename T1 = Default, typename T2 = Default, typename T3 = Default>
class Test {
};

用户想要修改第三个默认实参,则需要:

Test<Default, Default, UserType>

如果模板参数更多的情况下则需要将前n-1个模板实参指定,直到真正想要修改的第n个模板参数,因此希望实现如下效果,指定模板形参T2修改为UserType,而其他仍然保持Default参数;

Test<Place3<UserType>>

本文介绍两种实现方式,使用上稍有不同。

借助std::tuple实现

既然是类型的转换,那可以通过std::tuple来实现,笔者实现代码如下:

struct DefaultType {
    static void Process()
    {
        DBG_LOG("In Default");
    }
};

template<size_t v1, size_t v2>
struct IsSameVal : std::false_type {};
template<size_t v>
struct IsSameVal<v, v>: std::true_type {};

template<size_t index, typename...PlaceArgs>
struct GetPlace {
    using Type = DefaultType; // 可扩展每个index有不同的Default
};
template<size_t index, typename Place, typename...PlaceArgs>
struct GetPlace<index, Place, PlaceArgs...> {
    using Type = std::conditional_t<IsSameVal<index, Place::index>::value, typename Place::Type, typename GetPlace<index, PlaceArgs...>::Type>;
};

template<typename T1, typename T2, size_t curNum, size_t maxArgsNum>
struct ExpandImpl;

template<typename ...Args, typename ...PlaceArgs, size_t curNum, size_t maxArgsNum>
struct ExpandImpl<std::tuple<Args...>, std::tuple<PlaceArgs...>, curNum, maxArgsNum> {
    using Type = typename ExpandImpl<std::tuple<Args..., typename GetPlace<curNum, PlaceArgs...>::Type>, std::tuple<PlaceArgs...>, curNum + 1, maxArgsNum>::Type;
};

template<typename ...Args, typename ...PlaceArgs, size_t maxArgsNum>
struct ExpandImpl<std::tuple<Args...>, std::tuple<PlaceArgs...>, maxArgsNum, maxArgsNum> {
    using Type = std::tuple<Args...>;
};

#define PLACE(Index) \
template<typename T> \
struct Place ## Index { \
    using Type = T; \
    static constexpr size_t index = Index;\
}

#define MAX_ARGS_NUM (4)
template<typename ...Args>
using Expand = typename ExpandImpl<std::tuple<>, std::tuple<Args...>, 0, MAX_ARGS_NUM>::Type;

模板实参用户通过Expand<Place2<Custom>, Place0<Custom>>便可以指定对应位置的模板形参类型,其他未指定的则为默认Default类型;

PLACE(0);
PLACE(1);
PLACE(2);
PLACE(3);

template<typename T>
struct Test;

template<typename T0, typename T1, typename T2, typename T3>
struct Test<std::tuple<T0, T1, T2, T3>> {
    void Process()
    {
        T0::Process();
        T1::Process();
        T2::Process();
        T3::Process();
    }
};

struct Custom {
    static void Process()
    {
        DBG_LOG("In Custom");
    }
};

struct Custom2 {
    static void Process()
    {
        DBG_LOG("In Custom2");
    }
};

int main()
{
    {
        Test<Expand<>> t{};
        t.Process(); 
    }
    {
        Test<Expand<Place2<Custom>, Place0<Custom>>> t{};
        t.Process();
    }
    {
        Test<Expand<Place2<Custom2>, Place0<Custom>>> t{};
        t.Process();
    }
}
// 输出
2023-4-15:17:48:41.305502[DBG  ][Process:13] In Default 
2023-4-15:17:48:41.306385[DBG  ][Process:13] In Default
2023-4-15:17:48:41.306683[DBG  ][Process:13] In Default
2023-4-15:17:48:41.306966[DBG  ][Process:13] In Default
// Test<Expand<Place2<Custom>, Place0<Custom>>>,将T0和T2替换为Custom
2023-4-15:17:48:41.307259[DBG  ][Process:78] In Custom
2023-4-15:17:48:41.307507[DBG  ][Process:13] In Default
2023-4-15:17:48:41.307794[DBG  ][Process:78] In Custom
2023-4-15:17:48:41.308093[DBG  ][Process:13] In Default
// Test<Expand<Place2<Custom>, Place0<Custom>>>,将T0替换Custom,T2替换为Custom2
2023-4-15:17:48:41.308333[DBG  ][Process:78] In Custom
2023-4-15:17:48:41.308555[DBG  ][Process:13] In Default
2023-4-15:17:48:41.308895[DBG  ][Process:85] In Custom2
2023-4-15:17:48:41.309204[DBG  ][Process:13] In Default

Named Template Arguments实现

本实现来自【2】,借助虚继承实现,但相比扩展性个人会选择上一节中的实现;

class DefaultPolicy1 {};
class DefaultPolicy2 {};
class DefaultPolicy3 {
public:
	static void doPrint()
	{
		std::cout << "In DefaultPolicy3" << std::endl;
	}
};
class DefaultPolicy4 {};

// name default policies as P1, P2, P3, P4 
class DefaultPolicies {
public:
	using P1 = DefaultPolicy1;
	using P2 = DefaultPolicy2;
	using P3 = DefaultPolicy3;
	using P4 = DefaultPolicy4;
};

// class to define a use of the default policy values 
// avoids ambiguities if we derive from DefaultPolicies more than once 
class DefaultPolicyArgs : virtual public DefaultPolicies {
};

template<typename Policy> 
class Policy1_is : virtual public DefaultPolicies {
public: 
	using P1 = Policy; // overriding type alias 
};

template<typename Policy> 
class Policy2_is : virtual public DefaultPolicies {
public: 
	using P2 = Policy; // overriding type alias 
};

template<typename Policy> 
class Policy3_is : virtual public DefaultPolicies {
public: 
	using P3 = Policy; // overriding type alias 
};

template<typename Policy> 
class Policy4_is : virtual public DefaultPolicies {
public: 
	using P4 = Policy; // overriding type alias
};


// PolicySelector<A,B,C,D> creates A,B,C,D as base classes 
// Discriminator<> allows having even the same base class more than once 
template<typename Base, int D>
class Discriminator : public Base {
};

template<
	typename Setter1,
	typename Setter2,
	typename Setter3,
	typename Setter4>
class PolicySelector :
	public Discriminator<Setter1, 1>,
	public Discriminator<Setter2, 2>,
	public Discriminator<Setter3, 3>,
	public Discriminator<Setter4, 4> {
};

template<
	typename PolicySetter1 = DefaultPolicyArgs,
	typename PolicySetter2 = DefaultPolicyArgs,
	typename PolicySetter3 = DefaultPolicyArgs,
	typename PolicySetter4 = DefaultPolicyArgs>
class BreadSlicer {
	// use Policies::P1, Policies::P2, … to refer to the various policies 
	using Policies = PolicySelector<PolicySetter1, PolicySetter2, PolicySetter3, PolicySetter4>;

public: 
	void print()
	{ 
		Policies::P3::doPrint(); 
	}
};


class Custom {
public:
	static void doPrint()
	{
		std::cout << "In Custom" << std::endl;
	}
};

int main()
{
	BreadSlicer<> obj1;
	obj1.print();  // Default实现

	BreadSlicer<Policy3_is<Custom>> obj2;
	obj2.print();  // Custom实现
}

参考资料

【1】https://en.cppreference.com/w/cpp/language/default_arguments

【2】C++ Templates

【3】https://zhuanlan.zhihu.com/p/585183393

【3】路漫漫其修远兮,吾将上下而求索

【版权声明】本文内容来自摩杜云社区用户原创、第三方投稿、转载,内容版权归原作者所有。本网站的目的在于传递更多信息,不拥有版权,亦不承担相应法律责任。如果您发现本社区中有涉嫌抄袭的内容,欢迎发送邮件进行举报,并提供相关证据,一经查实,本社区将立刻删除涉嫌侵权内容,举报邮箱: cloudbbs@moduyun.com

  1. 分享:
最后一次编辑于 2023年11月08日 0

暂无评论

推荐阅读
  ZXL8ALkrtBPG   2023年11月02日   48   0   0 cpp
  PQYWJLBjS0G7   2023年11月02日   48   0   0 cpp
  ZXL8ALkrtBPG   2023年11月02日   66   0   0 cpp
  ZXL8ALkrtBPG   2023年11月02日   39   0   0 cpp
  ZXL8ALkrtBPG   2023年11月02日   39   0   0 cpp
  ZXL8ALkrtBPG   2023年11月02日   46   0   0 cpp
  ZXL8ALkrtBPG   2023年11月02日   76   0   0 cpp
  ZXL8ALkrtBPG   2023年11月02日   31   0   0 cpp
  ZXL8ALkrtBPG   2023年11月02日   26   0   0 cpp
  ZXL8ALkrtBPG   2023年11月02日   33   0   0 cpp
  ZXL8ALkrtBPG   2023年11月19日   18   0   0 cpp
  ZXL8ALkrtBPG   2023年11月02日   54   0   0 cpp
  ZXL8ALkrtBPG   2023年11月02日   68   0   0 Listcpp
  ZXL8ALkrtBPG   2023年11月02日   46   0   0 cpp
  ZXL8ALkrtBPG   2023年11月02日   66   0   0 cpp
  ZXL8ALkrtBPG   2023年11月19日   31   0   0 cpp
ZXL8ALkrtBPG