平滑过渡
问题描述:
考虑以下情况,其中一个基类规定,可以在派生类中重新实现的功能:平滑过渡
template<typename Impl>
class Base {
public:
void f(double& value, double param)
{ value = 0.0; }
};
class Implementation : public Base<Implementation> {
public:
void f(double& value, double param)
{ value = param; }
};
int main()
{
Implementation imp;
double value = 0.0;
imp.f(value, 1.0);
}
假设我想改变功能f
在签名基类到double f(double param)
并将其用于Base
和派生类。我想在不立即破坏所有实现类的情况下执行此更改。相反,实现类的提供者应该得到弃用警告。我想出了以下情况:
template<typename Impl>
class Base {
public:
double f(double param) __attribute__ ((deprecated))
{
double v = 0.0;
static_cast<Impl*>(this)->f(v, param);
return v;
}
void f(double& value, double param)
{ value = 0.0; }
};
// class Implementation as before
int main()
{
Implementation imp;
double value = imp.f(1.0);
}
其结果是,在Base
版本的f
只应如果f
不Implementation
与新的签名重新实现,得到所需的弃用警告调用。
很显然,这并不能编译,因为在Implementation
阴影的f
定义f
在Base
:
error: no matching function for call to ‘Implementation::f(double)’
double value = imp.f(1.0);
一个解决办法是增加using
:
class Implementation : public Base<Implementation> {
public:
using Base<Implementation>::f;
void f(double& value, double param)
{ value = param; }
};
这将迫使提供者Implementation
立即改变他的代码,这正是应该首先避免的。另一种可能性是在新签名中更改名称f
。我也想避免这个,因为f
在我的真实使用案例中有一个非常好的名字。
所以我的问题是:我可以进行签名更改
- 使得
Implementation
提供商获得弃用警告,但他们的代码不会立即断裂,即,在不改变Implementation
,并 - 而不必重命名
f
?
答
我可以构建从jrok's answer对我的作品的问题Check if a class has a member function of a given signature的解决方案:
#include <iostream>
#include <type_traits>
template<typename, typename T>
struct hasFunctionF {};
template<typename C, typename Ret, typename... Args>
struct hasFunctionF<C, Ret(Args...)> {
private:
template<typename T>
static constexpr auto check(T*)
-> typename
std::is_same<
decltype(std::declval<T>().f(std::declval<Args>()...)),
Ret // ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
>::type; // attempt to call it and see if the return type is correct
template<typename>
static constexpr std::false_type check(...);
typedef decltype(check<C>(0)) type;
public:
static constexpr bool value = type::value;
};
template<typename Impl>
class Base {
public:
template<class T = Impl>
__attribute__ ((deprecated))
const typename std::enable_if_t<hasFunctionF<T, void(double&, double)>::value, double> g(double param)
{
double v = 0.0;
static_cast<Impl*>(this)->f(v, param);
return v;
}
template<class T = Impl>
const typename std::enable_if_t<hasFunctionF<T, double(double)>::value, double> g(double param)
{
return static_cast<Impl*>(this)->f(param);
}
};
class OldImplementation : public Base<OldImplementation> {
public:
void f(double& value, double param)
{ value = param; }
};
class NewImplementation : public Base<NewImplementation> {
public:
double f(double param)
{ return param; }
};
int main()
{
OldImplementation oldImp;
double value = oldImp.g(1.0);
std::cout << "old: " << value << std::endl;
NewImplementation newImp;
std::cout << "new: " << newImp.g(1.0) << std::endl;
}
我还是要在基类中引入一个新的函数名g
,并使用该名称调用。但我不认为这可以在不改变用户代码的情况下避免。用户代码是OldImplementation
,不需要立即更改。编译使用旧界面的用户代码只会提供弃用警告。这正是我想要实现的。
只是一个问题,与问题无关:为什么接口中的方法不是标记为“虚拟”?如果方法不能被覆盖,接口的用途是什么?或者,更好的是,它为什么会抛出一个异常,而不是被标记为纯虚拟的(这是用C++编写接口的首选方式)。 –
“使用”解决方案有什么问题? –
这是具有更复杂的类相互依赖性的真实案例的简化版本。通过使用CRTP,静态绑定优于动态绑定。我尽可能地把它煮沸,同时保持本质。 –