Неявное приведение класса-оболочки к суперклассу в шаблонном вызове

При разработке DSL (который компилируется в C++) я счел удобным определить класс-оболочку, который после уничтожения вызывал бы метод .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() могут иметь разную семантику!

Все это прекрасно работает до тех пор, пока обернутый класс не используется в качестве члена не связанного с ссылкой шаблонного вызова, после чего создается экземпляр freeOnDestroy, вызывающий free для обернутого объекта. Я хотел бы, чтобы метод tempated использовал T вместо freeOnDestroy<T> и неявно приводил параметр к суперклассу. Следующий пример кода иллюстрирует эту проблему:

// 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).

Таким образом, мой вопрос таков: существует ли чистый метод, обеспечивающий приведение базового класса к его суперклассу при разрешении шаблона? Или, если нет, есть ли способ использовать SFINAE, вызывая ошибку подстановки, когда аргумент шаблона является экземпляром класса-оболочки, и, таким образом, заставляя его использовать неявное приведение к обернутому классу (без дублирования каждого foo-подобного сигнатура метода, возможно, десятки раз)?

В настоящее время у меня есть обходной путь, который включает изменения в DSL, но я не совсем доволен им, и мне было любопытно, возможно ли вообще разработать класс-оболочку, который работает, как описано.


person André Harder    schedule 02.07.2016    source источник


Ответы (3)


Проблема здесь не в том, когда «обернутый класс используется в качестве члена для вызова шаблона без ссылки».

Проблема здесь в том, что оболочка шаблона — и, вероятно, его суперкласс — нарушила Правило трех.

Передача экземпляра класса в качестве нессылочного параметра - это просто еще один способ сказать "передача по значению". Передача по значению создает копию экземпляра класса. Ни ваш шаблонный класс, ни его обернутый класс, скорее всего, не имеют явного конструктора копирования; как таковой, скопированный экземпляр класса не знает, что он является копией, поэтому деструктор делает то, что, по его мнению, он должен делать.

Правильное решение здесь не состоит в том, чтобы взломать что-то, что делает передачу экземпляра freeOnDestroy<T> по значению в конечном итоге копированием T, а не freeOnDestroy<T>. Правильное решение — добавить правильный конструктор копирования и оператор присваивания как к шаблону freeOnDestroy, так и, возможно, к любому суперклассу, который его использует, чтобы все соответствовало Правилу Трех.

person Sam Varshavchik    schedule 02.07.2016
comment
Не думал об этой альтернативе, и да, добавление bool isCopy и инициализация/проверка его соответствующим образом для freeOnDestroy действительно решает проблему, хотя за счет дополнительного логического значения и ветки для каждого возврата вызываемой функции - вероятно, нет ужасно дорого (хотя я немного обеспокоен тем, что дополнительный размер может привести к смещению блоков и сбить с толку оптимизатор компилятора С++, который тогда может быть ОЧЕНЬ дорогим). Что касается хорошего дизайна, то это правильное решение. - person André Harder; 03.07.2016

Вы можете использовать правильно определенный детектор и функцию 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 вызывается только один раз, то есть для 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); для принудительного использования типа.
Удалите его, если хотите разрешить такое выражение. Из вопроса было непонятно.

person skypjack    schedule 03.07.2016
comment
Запрещать любое использование обернутых классов на самом деле нежелательно, поскольку цель состоит в том, чтобы они использовались в вызовах функций, как если бы они были обернутым классом T, а не запрещали их полностью! Как упоминалось в вопросе, создание in ссылки, такой как foo(T &in), действительно решает непосредственную проблему (даже без явного обнаружения оболочки!), При этом теряется семантика передачи по значению, которая может потребоваться для foo для правильной работы. Обратите внимание, что, как и ожидалось, удаление & из вашего второго определения foo возвращает код к предыдущему поведению с двойным вызовом .free(). - person André Harder; 03.07.2016
comment
@AndréHarder Ну да, но в примере есть две sfinaed foo функции. Первый получает свой параметр путем копирования по мере необходимости, а второй получает его по ссылке. Обратите внимание, что последний определяет, существует ли уже оболочка freeOnDestroy. - person skypjack; 03.07.2016
comment
Это правда, и это определенно лучше, чем использование одного foo(T &in)! Тем не менее, мы по-прежнему теряем семантику передачи по значению, когда у нас есть оболочка. - person André Harder; 03.07.2016
comment
@AndréHarder Вы можете удалить & и решить, что там делать. Само собой разумеется, что если вы скопируете обертку, вы удалите данные дважды. Например, вы можете отключить вызов free всякий раз, когда обнаружите, что обертка использовалась в качестве параметра, например. В основном это зависит от программного обеспечения, над которым вы работаете, я не могу сказать. - person skypjack; 03.07.2016

Одно из решений, которое, хотя и выглядит немного уродливым, похоже, работает, заключается в использовании перегруженного метода развертывания, такого как:

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. Лучшие решения и улучшения приветствуются!

person André Harder    schedule 03.07.2016