C++ 使用typename来修复模板编译错误

问题由来

在看Unreal Engine源码时,有一行代码我比较疑惑:

// 这是我要调用的代码
// Called to bind functionality to input
void AMyCharacter::SetupPlayerInputComponent(UInputComponent* PlayerInputComponent)
{
    
    
	Super::SetupPlayerInputComponent(PlayerInputComponent);
	
	// 我需要创建一个对应函数签名的函数, 叫MovingForwardFunc
	PlayerInputComponent->BindAxis<AMyCharacter>(FName("MovingForward"), this, &MovingForwardFunc);
}

下面是这些函数的定义:

/**
 * Binds a delegate function an Axis defined in the project settings.
 * Returned reference is only guaranteed to be valid until another axis is bound.
 */
template<class UserClass>
FInputAxisBinding& BindAxis( const FName AxisName, UserClass* Object, typename FInputAxisHandlerSignature::TMethodPtr< UserClass > Func )
{
    
    
	FInputAxisBinding AB( AxisName );
	AB.AxisDelegate.BindDelegate(Object, Func);
	AxisBindings.Emplace(MoveTemp(AB));
	return AxisBindings.Last();
}


/* Helper typedefs for getting a member function pointer type for the delegate with a given payload */
template <typename UserClass, typename... VarTypes> using TMethodPtr      = typename TMemFunPtrType<false, UserClass, RetValType(ParamTypes..., VarTypes...)>::Type;

疑惑的点在于,下面这一块代码是作为函数签名里的一个参数出现的:

typename FInputAxisHandlerSignature::TMethodPtr< UserClass >

因为在我之前的理解,typename是只能用于template的尖括号里的,所以写下这篇文章研究一下。

参考: Why typename?

使用typename来修复模板编译错误

先看个例子:

struct Empty {
    
    };

struct MyRandomClass
{
    
    
	using E = Empty;
};

template<typename T>
struct TemplateExample 
{
    
    
	T m_T;
	T::E m_Empty;
};

想象的用法是,当T为MyRandomClass时,模板实例化为:

// 这样看,没有问题
struct TemplateExample 
{
    
    
	MyRandomClass m_T;
	MyRandomClass::E m_Empty;
};

然后,上面的模板直接编译会报错,报错位置为T::E m_Empty;,报错信息为:

//  先给警告
warning C4346: 'E': dependent name is not a type
// 建议你加个typename
message : prefix with 'typename' to indicate a type
// 让你去查查TemplateExample的编译过程
message : see reference to class template instantiation 'TemplateExample<T>' being compiled

// 再给Error
error C2061: syntax error: identifier 'E'
error C2238: unexpected token(s) preceding ';'

Error信息很明确,这里的E,模板函数是认不出来的,因为它把E当成了变量,而不是Type。至于具体怎么改,警告其实已经告诉我了,改成:

template<typename T>
struct TemplateExample 
{
    
    
	T m_T;
	typename T::E m_Empty;
};

所以核心原因是,编译器会默认认为Template的scope里面(即花括号里的内容)的所有出现的identifier是value,而不是类型(可以认为任何人为命名的东西都是identifier)。这个问题还有别的变种:
在这里插入图片描述
看这种情况,当编译器碰到Remains时,它默认认为这是变量,那么就会报错,应该是因为变量后面不可以接<FoodT>,这里加个template关键字即可。

最后说一种特殊情况,下面这种写法是不需要加typename的,因为它是在类声明里出现的。编译器认为所有的类声明出现的东西,都是类型,而不是变量,所以这里不需要加typename:
在这里插入图片描述


顺便介绍一些相关编译报错出现的概念名词

Dependent scope type和dependent name

参考:https://mainfunda.com/what-are-dependent-scope-type-in-templates/
参考:https://www.ibm.com/docs/en/zos/2.2.0?topic=only-name-binding-dependent-names-c

C++里所有的变量、数据都有自己的type,但是C++里有一种type,由于它的类型依赖于模板参数的类型,所以叫做dependent type。比如下面这个:

template<typename T>
struct MFPointer 
{
    
    
    typedef T* ptype;// ptype是T*类型的别名
};

这里的ptype的类型就是dependent scope datatype,它需要通过scope resolution operator(即::)来获取,如果T为int,则ptype则是int*,写法如下:

MFPointer<int>::ptype pi = nullptr;
MFPointer<float>::ptype pf = nullptr;

dependent name也是类似的,参考:https://en.cppreference.com/w/cpp/language/dependent_name


解析函数签名类型

最后再把开头的问题解决一下:

PlayerInputComponent->BindAxis<AMyCharacter>(FName("MovingForward"), this, &MovingForwardFunc);

/**
 * Binds a delegate function an Axis defined in the project settings.
 * Returned reference is only guaranteed to be valid until another axis is bound.
 */
template<class UserClass>
FInputAxisBinding& BindAxis( const FName AxisName, UserClass* Object, typename FInputAxisHandlerSignature::TMethodPtr< UserClass > Func )
{
    
    
	FInputAxisBinding AB( AxisName );
	AB.AxisDelegate.BindDelegate(Object, Func);
	AxisBindings.Emplace(MoveTemp(AB));
	return AxisBindings.Last();
}

模板实例化为:

FInputAxisBinding& BindAxis(const FName AxisName, AMyCharacter* Object, typename FInputAxisHandlerSignature::TMethodPtr<AMyCharacter> Func)
{
    
    
	...
}

根据:

/* Helper typedefs for getting a member function pointer type for the delegate with a given payload */
template <typename UserClass, typename... VarTypes> using TMethodPtr = typename TMemFunPtrType<false, UserClass, RetValType(ParamTypes..., VarTypes...)>::Type;

可以解析出FInputAxisHandlerSignature::TMethodPtr<AMyCharacter> Func,而关于FInputAxisHandlerSignature又有一大堆东西:


FInputAxisHandlerSignature

/** 
 * Delegate signature for axis handlers. 
 * @AxisValue: "Value" to pass to the axis.  This value will be the device-dependent, so a mouse will report absolute change since the last update, 
 *		a joystick will report total displacement from the center, etc.  It is up to the handler to interpret this data as it sees fit, i.e. treating 
 *		joystick values as a rate of change would require scaling by frametime to get an absolute delta.
 */
DECLARE_DELEGATE_OneParam( FInputAxisHandlerSignature, float );

// Multiple-parameter versions of above delegate types:
#define DECLARE_DELEGATE_OneParam( DelegateName, Param1Type ) FUNC_DECLARE_DELEGATE( DelegateName, void, Param1Type )

/**
 * Declares a delegate that can only bind to one native function at a time
 *
 * @note: The last parameter is variadic and is used as the 'template args' for this delegate's classes (__VA_ARGS__)
 * @note: To avoid issues with macro expansion breaking code navigation, make sure the type/class name macro params are unique across all of these macros
 */
#define FUNC_DECLARE_DELEGATE( DelegateName, ReturnType, ... ) \
	typedef TDelegate<ReturnType(__VA_ARGS__)> DelegateName;
	

// TDelegate函数的模板特化版本
/**
 * Unicast delegate template class.
 *
 * Use the various DECLARE_DELEGATE macros to create the actual delegate type, templated to
 * the function signature the delegate is compatible with. Then, you can create an instance
 * of that class when you want to bind a function to the delegate.
 */
template <typename DelegateSignature, typename UserPolicy = FDefaultDelegateUserPolicy>
class TDelegate
{
    
    
	static_assert(sizeof(DelegateSignature) == 0, "Expected a function signature for the delegate template parameter");
};

template <typename InRetValType, typename... ParamTypes, typename UserPolicy>
class TDelegate<InRetValType(ParamTypes...), UserPolicy> : public TDelegateBase<UserPolicy>
{
    
    
	// 所以TMethodPtr源自TDelegate,
	/* Helper typedefs for getting a member function pointer type for the delegate with a given payload */
	template <typename UserClass, typename... VarTypes> using TMethodPtr      = typename TMemFunPtrType<false, UserClass, 
	RetValType(ParamTypes..., VarTypes...)>::Type;
	...
}

这里可以把DECLARE_DELEGATE_OneParam( FInputAxisHandlerSignature, float );展开为:

typedef TDelegate<void(float)> FInputAxisHandlerSignature;

也就是说,FInputAxisHandlerSignature TDelegate<void(float)>类型的typedef,所以FInputAxisHandlerSignature::TMethodPtr<AMyCharacter> 相当于:

TDelegate<void(float)> TMethodPtr<AMyCharacter>

展开TMethodPtr

继续往上展开,难点在于这里TMethodPtr类里的Type是什么类型

TDelegate<void(float)> TMemFunPtrType<false, AMyCharacter>::Type

// 而这里的Type是在TMemFunPtrType类里定义的函数指针
template <typename Class, typename RetType, typename... ArgTypes>
struct TMemFunPtrType<false, Class, RetType(ArgTypes...)>
{
    
    
	// Type是个函数指针,  Class::*是意思难道必须是Class内部的成员函数的指针?
	typedef RetType (Class::* Type)(ArgTypes...);
};

所以最后的函数签名应该是:

TDelegate<void(float)> AMyCharacter::Func;

这里的TDelegate就不深究了,其实应该是传入一个TDelegate对象,这个对象是AMyCharacter的成员函数的Wrapper,函数签名为(void(float))。

总之,这里输入的函数,返回值为void,参数为float,而且要是AMyChracter的成员函数,这里不能直接传入函数的指针,而是要通过TDelegate对象传入,不过我猜这里可以直接隐式转换,所以这么写了,果然是OK的:

PlayerInputComponent->BindAxis<AMyCharacter>(FName("MovingForward"), this, &AMyCharacter::MovingForwardFunc);

顺便说一句,感觉自己推断类型很麻烦,如果只是想知道函数签名,可以让他编译错误,看看提示是啥,比如我这么写,编译错误:

PlayerInputComponent->BindAxis<AMyCharacter>(FName("MovingForward"), this, &/*AMyCharacter::*/MovingForwardFunc);

错误信息告诉了我函数签名:

2>E:\UnrealProjects\MyCppDemo\Source\MyCppDemo\Private\MyCharacter.cpp(53): error C2276: '&': illegal operation on bound member function expression
2>E:\UnrealProjects\MyCppDemo\Source\MyCppDemo\Private\MyCharacter.cpp(53): error C2672: 'UInputComponent::BindAxis': no matching overloaded function found
2>E:\UnrealProjects\MyCppDemo\Source\MyCppDemo\Private\MyCharacter.cpp(53): error C2780: 'FInputAxisBinding &UInputComponent::BindAxis(const FName,UserClass *,TMemFunPtrType<false,UserClass,void(float)>::Type)': expects 3 arguments - 2 provided
2>E:\UE_5.0\UE_5.0\Engine\Source\Runtime\Engine\Classes\Components\InputComponent.h(906): note: see declaration of 'UInputComponent::BindAxis'

猜你喜欢

转载自blog.csdn.net/alexhu2010q/article/details/125771254