在模板调用中隐式地将 wrapper class 转换为 superclass

Implicitly cast wrapper class to supperclass in templated call

在设计 DSL(编译成 C++)时,我发现定义一个包装器 class 很方便,一旦销毁,将在包含的 [=56= 上调用 .free() 方法]:

template<class T> 
class freeOnDestroy : public T {
    using T::T;
public:
    operator T&() const { return *this; }
    ~freeOnDestroy() { T::free(); }
};

包装器设计为完全透明:所有方法、重载和构造函数都继承自 T(至少据我所知),但是当包含在包装器中时,将调用 free() 方法毁灭后。请注意,我明确避免为此使用 T 的析构函数,因为 T::free()~T() 可能具有不同的语义!

所有这些工作正常,直到包装的 class 被用作非引用模板化调用的成员,此时 freeOnDestroy 被实例化,在包装的对象上免费调用。我希望 发生的是让模板方法使用 T 而不是 freeOnDestroy<T>,并将参数隐式转换为 supperclass .下面的代码示例说明了这个问题:

// First class that has a free (and will be used in foo)
class C{
    int * arr;
public:
    C(int size){ 
        arr = new int[size]; 
        for (int i = 0; i < size; i++) arr[i] = i;
    }
    int operator[] (int idx) { return arr[idx]; }
    void free(){ cout << "free called!\n"; delete []arr; }
};

// Second class that has a free (and is also used in foo)
class V{
    int cval;
public:
    V(int cval) : cval(cval) {}
    int operator[] (int idx) { return cval; }
    void free(){}   
};

// Foo: in this case, accepts anything with operator[int]
// Foo cannot be assumed to be written as T &in!
// Foo in actuality may have many differently-templated parameters, not just one
template<typename T>
void foo(T in){
    for(int i = 0; i < 5; i++) cout << in[i] << ' ';
    cout << '\n';
}

int main(void){
    C c(15);
    V v(1);
    freeOnDestroy<C> f_c(15);
    foo(c); // OK!
    foo(v); // OK!
    foo<C>(f_c); // OK, but the base (C) of f_c may not be explicitly known at the call site, for example, if f_c is itself received as a template
    foo(f_c); // BAD: Creates a new freeOnDestroy<C> by implicit copy constructor, and uppon completion calls C::free, deleting arr! Would prefer it call foo<C>
    foo(f_c); // OH NO! Tries to print arr, but it has been deleted by previous call! Segmentation fault :(
    return 0;
}

我应该提到的一些非解决方案是:

总而言之,我的问题是:是否有一种干净的方法来确保在解析模板时将基 class 转换为其超 class?或者,如果没有,是否有某种使用 SFINAE 的方法,当模板参数是包装器 class 的实例时导致替换失败,从而强制它使用隐式转换到包装器 class(无需重复每个 foo-like 方法签名可能数十次)?

我目前有一个涉及 DSL 更改的工作区,但我对此并不完全满意,并且很好奇是否有可能设计一个包装器 class 作为描述。

这里的问题不是 "wrapped class gets used as a member to a non-reference templated call"。

这里的问题是模板包装器——可能还有它的超级class——有violated the Rule Of Three.

将 class 的实例作为非引用参数传递只是 "passing by value" 的另一种说法。按值传递会复制 class 的实例。您的模板 class 及其包装的 class 很可能都没有显式复制构造函数;因此,class 的复制实例不知道它是一个副本,因此析构函数执行它认为应该执行的操作。

这里正确的解决方案不是破解某些东西,使按值传递 freeOnDestroy<T> 的实例最终复制 T,而不是 freeOnDestroy<T>。正确的解决方案是向 freeOnDestroy 模板以及可能使用它的任何 superclass 添加适当的复制构造函数和赋值运算符,以便一切都符合三规则。

您可以使用正确定义的检测器和 sfinaed 函数,如下所示:

#include<iostream>
#include<type_traits>

template<class T> 
class freeOnDestroy : public T {
    using T::T;
public:
    operator T&() const { return *this; }
    ~freeOnDestroy() { T::free(); }
};

template<typename T>
struct FreeOnDestroyDetector: std::false_type { };

template<typename T>
struct FreeOnDestroyDetector<freeOnDestroy<T>>: std::true_type { };

class C{
    int * arr;
public:
    C(int size){ 
        arr = new int[size]; 
        for (int i = 0; i < size; i++) arr[i] = i;
    }
    int operator[] (int idx) { return arr[idx]; }
    void free(){ std::cout << "free called!\n"; delete []arr; }
};

class V{
    int cval;
public:
    V(int cval) : cval(cval) {}
    int operator[] (int idx) { return cval; }
    void free(){}   
};

template<typename..., typename T>
std::enable_if_t<not FreeOnDestroyDetector<std::decay_t<T>>::value>
foo(T in) {
    std::cout << "here you have not a freeOnDestroy based class" << std::endl;
}

template<typename..., typename T>
std::enable_if_t<FreeOnDestroyDetector<std::decay_t<T>>::value>
foo(T &in) {
    std::cout << "here you have a freeOnDestroy based class" << std::endl;
}

int main(void){
    C c(15);
    V v(1);
    freeOnDestroy<C> f_c(15);
    foo(c);
    foo(v);
    foo<C>(f_c);
    foo(f_c);
    foo(f_c);
    return 0;
}

正如您在 运行 示例中看到的那样,free 仅被调用一次,即针对在 main 函数中创建的 freeOnDestroy
如果你想明确禁止freeOnDestroy作为参数,你可以使用单个函数作为以下函数:

template<typename..., typename T>
void foo(T &in) {
    static_assert(not FreeOnDestroyDetector<std::decay_t<T>>::value, "!");
    std::cout << "here you have a freeOnDestroy based class" << std::endl;
}

请注意,我添加了一个可变参数作为保护,这样就不能再使用 foo<C>(f_c); 强制使用类型。
如果您想允许这样的表达式,请将其删除。从问题上看不清楚。

一个解决方案,虽然有点难看,但似乎有效,是使用重载展开方法,例如:

template<typename T> T freeOnDestroyUnwrapper(const T &in){ return in; }
template<typename T> T freeOnDestroyUnwrapper(const freeOnDestroy<T> &in){ return in; }
template<typename T> T freeOnDestroyUnwrapper(const freeOnDestroy<typename std::decay<T>::type> &in){ return in; }
template<typename T> T& freeOnDestroyUnwrapper(T &in){ return in; }
template<typename T> T& freeOnDestroyUnwrapper(freeOnDestroy<T> &in){ return in; }
template<typename T> T& freeOnDestroyUnwrapper(freeOnDestroy<typename std::decay<T>::type> &in){ return in; }

然后,可以使用解包器进行调用:

int main(void){
    C c(15);
    V v(1);
    freeOnDestroy<C> f_c(15);
    foo(freeOnDestroyUnwrapper(c));
    foo(freeOnDestroyUnwrapper(v));
    foo<C>(freeOnDestroyUnwrapper(f_c));
    foo(freeOnDestroyUnwrapper(f_c));
    foo(freeOnDestroyUnwrapper(f_c));
    return 0;
}

或者,为了不那么冗长,我们可以更改 foo 以便它为我们执行此操作:

template<typename T>
void _foo(T in){
    for(int i = 0; i < 5; i++) cout << in[i] << ' ';
    cout << '\n';
}
template<typename... Ts>
void foo(Ts&&... args){
    _foo(freeOnDestroyUnwrapper(args)...);
}

然后正常调用:

int main(void){
    C c(15);
    V v(1);
    freeOnDestroy<C> f_c(15);
    foo(c);
    foo(v);
    //foo<C>(f_c); // This now doesn't work!
    foo(f_c);
    foo(f_c);
    return 0;
}

这似乎适用于 foo 可能具有的任意数量的参数(不同模板,如果需要的话),并且当 foos 输入是一个引用时(这在我的上下文中不会发生)似乎表现得很好,但为了使此解决方案通用化会更好)。

我不相信这是最好的解决方案,或者它可以推广到所有情况,另外,必须将所有声明加倍有点麻烦,而且对大多数 IDE 的自动完成功能来说是不透明的。欢迎更好的解决方案和改进!