在模板化调用

时间:2016-07-02 23:35:39

标签: c++ templates sfinae

在设计DSL(编译成C ++)时,我发现定义一个包装类很方便,uppon destroy会在包含的类上调用.free()方法:

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

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

所有这一切都很好,直到一个包装类被用作非引用模板化调用的成员,此时freeOnDestroy被实例化,在包装对象上调用free。我将喜欢发生的事情是使用T而不是freeOnDestroy<T>的tempated方法,并隐式地将参数强制转换为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;
}

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

  • 使freeOnDestroy::freeOnDestroy(const freeOnDestroy &src)显式且隐私,但这似乎会覆盖T的构造函数。我希望它会尝试将其隐式转换为T并将其用作模板参数。
  • 假设foo收到其模板化参数的引用(如void foo(T &in)中所示:既不是这种情况,也不是某些情况下可取的
  • 始终明确地将调用模板化为foo,与foo<C>(f_c)中一样:f_c本身可能会被模板化,因此很难通过foo来实例化C foo(是的,这可以通过创建foo的多个版本来完成,逐个删除包装器,但是我无法找到一种方法来执行此操作而不会创建不同的重载每个模板化的foo}参数。

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

我目前有一个涉及DSL变化的工作环节,但我对它并不完全满意,并且好奇是否可以设计一个按照描述的方式工作的包装类。

3 个答案:

答案 0 :(得分:1)

这里的问题不是“包装类被用作非参考模板化调用的成员”。

这里的问题是模板包装器 - 也可能是它的超类 - 具有violated the Rule Of Three

将类的实例作为非引用参数传递只是说“按值传递”的另一种方式。按值传递会生成类实例的副本。你的模板类 - 也不是它的包装类 - 很可能都有一个显式的复制构造函数;因此,类的复制实例不知道它是一个副本,因此析构函数会执行它认为应该执行的操作。

这里的正确解决方案是不要破解使得freeOnDestroy<T>的实例通过值最终复制T而不是freeOnDestroy<T>传递。正确的解决方案是向freeOnDestroy模板以及可能使用它的任何超类添加适当的复制构造函数和赋值运算符,以便所有内容都符合三规则。

答案 1 :(得分:0)

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

#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仅被调用一次,即freeOnDestroy函数中创建的main。 如果您想明确禁止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);来强制使用类型。
如果要允许这样的表达式,请将其删除。从问题中不清楚。

答案 2 :(得分:0)

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

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; }

然后,可以使用unwrapper进行调用:

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可能具有的任何数量的参数(如果需要的话,具有不同的模板),并且当foo的输入是引用时似乎表现得恰当(在我的上下文中不会发生,但是为了使这个解决方案通用,这将是好的。

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