Обертывание функций C в автообъектах без дублирования кода

В C++03, когда вы должны были обернуть кучу функций C в классе для создания "автообъекта", вы должны были настроить объект в соответствии с типом функций, которые он инкапсулировал. Например, чтобы обернуть файл Windows HANDLE, вам нужно было вызвать CloseHandle() в деструкторе и CreateFile() в конструкторе. Конструктор должен имитировать сигнатуру функции функции CreateFile(), без файловой переменной HANDLE (поскольку она управляется).

В любом случае, я хотел бы знать, возможно ли использовать новые функции C++11 для создания единого универсального класса, который можно использовать для переноса любого типа ресурса, предоставляя только реализацию для создания и удаления?

Одна проблема, которую я предвижу, состоит в том, что функция создания, такая как отмеченная выше с CreateFile(), может принимать любое количество параметров. Есть ли способ автоматически генерировать шаблонный конструктор, который имитирует сигнатуру функции? Параметры Variadic приходят на ум, но я еще не использовал их.

Кто-нибудь пробовал написать что-то подобное?

РЕДАКТИРОВАТЬ: Некоторый код, чтобы помочь проиллюстрировать (псевдо):

template<typename Res, FunctionPtrToCreatorFunc Func, typename... Arguments>
class creator
{
public:
  operator()(Res &r, Arguments... Args)
  {
    Func(r, /*use args?*/ Args); // Allocate resource, ie. CreateFile(r, args)
  }
};

template<typename Res, FunctionPtrToDeleterFunc Func>
class deleter
{
  operator()(Res &r)
  {
    Func(r); // delete the resource, ie. CloseHandle(r)
  }
};

Тогда это будет реализация моего супер автоматического объекта:

template<typename Res, typename Creator, typename Deleter>
class auto_obj
{
public:

  auto_obj(/*somehow copy Args from Creator class?*/)
  {
    Creator(_res, /*args?*/);
  }

  ~auto_obj()
  {
    deleter(_res);
  }

  Res _res;
};

Да, это имеет структуру, аналогичную shared_ptr или же unique_ptr, но вместо этого конструктор будет тем, который создает ресурсы, написанные разработчиком и создателем классов удаления. У меня есть ощущение, что std::bind может сыграть в этом роль, но я никогда этим не пользовался.

1 ответ

Вот удар в этом:

#include <utility>
#include <type_traits>
#include <cstddef>

Более дружелюбный способ обернуть функцию. Я перемещаю шаблон подписи к этому templateвместо того, чтобы испортить фактический класс RAII ниже. Это также позволяет использовать полноценные функциональные объекты, а также функции в классе RAII ниже:

template< typename FuncSig, FuncSig func >
struct Functor {
  template<typename... Args>
  auto operator()(Args&&... args) const
  -> decltype( func(std::forward<Args>(args)...) )
    { return ( func(std::forward<Args>(args)...) ); }
};

Одной операцией, которая необходима не только для основных функций, является возможность "обнулить" дескриптор, позволяя существовать недействительным дескрипторам и позволяя перемещать дескрипторы. Zeroer мой функциональный объект по умолчанию для "нулевого" дескриптора:

struct Zeroer {
  template<typename T>
  void operator()( T& t ) const {
    t = 0;
  }
};

RAII_handle саму себя. Вы упаковываете в него сигнатуры создания и уничтожения, и он направляет конструкцию к базовым данным. .close() позволяет закрыть RAII_handle рано, что является распространенным требованием на практике. Вы получаете доступ к основным данным через operator* или же operator->и в то время как это делает его похожим на указатель, RAII_handle не подчиняется семантике указателя. Это тип только для перемещения.

template< typename T, typename Creator, typename Destroyer, typename Nuller=Zeroer >
struct RAII_handle {
  RAII_handle( std::nullptr_t ):
    data()
  {
    Nuller()(data);
  }
  RAII_handle( RAII_handle const& ) = delete;
  RAII_handle( RAII_handle && o ):data(std::move(o.data)) {
    Nuller()(o.data);
  }
  RAII_handle& operator=( RAII_handle const& ) = delete;
  RAII_handle& operator=( RAII_handle && o ) {
    data = std::move(o.data);
    Nuller()(o.data);
    return *this;
  }
  template<typename... Args>
  RAII_handle( Args&&... args ):
    data( Creator()(std::forward<Args>(args)...) )
  {}
  auto close()->decltype( Destroyer()(std::declval<T&>()) ) {
    auto retval = Destroyer()(data);
    Nuller()(data);
    return retval;
  }
  ~RAII_handle() {
    close();
  }
  T& get() { return data; }
  T const& get() const { return data; }

  T& operator*() { return get(); }
  T const& operator*() const { return get(); }

  T* operator->() { return &get(); }
  T const* operator->() const { return &get(); }
private:
  T data;
};

Теперь немного тестового кода. Мои файловые дескрипторы будут unsigned charи открытие / закрытие просто проверит, если что-то не работает правильно.

#include <iostream>
typedef unsigned char HANDLE;
HANDLE CreateFile( char const* name ) {
  std::cout << name << "\n";
  return 7;
}
bool CloseFile( HANDLE h ) {
  if (h) {
    --h;
    std::cout << (int)h << "\n";
    return true;
  } else {
    std::cout << "already closed\n";
    return true;
  }
}

Если у вас есть функции открытия / закрытия или функциональные объекты, вот как вы делаете тип FileHandle:

typedef RAII_handle< HANDLE, Functor< HANDLE(*)( char const* ), CreateFile >, Functor< bool(*)(HANDLE), CloseFile > > FileHandle;

Вы можете поддерживать целые наборы перегрузки, просто создав функциональный объект, который перенаправляет на фиксированное имя функции, а не на фиксированный указатель функции. В основном взять Functor выше, удалите template подпись и указатель, и заменить использование func с фактическим использованием имени вашей функции.

Внезапно ваш объект функции представляет собой не вызов одной функции, а вызов всего набора перегрузки.

Любимая работа может даже поддерживать несколько функций, что позволяет одному объекту функции поддерживать вызов либо CreateFile или же CreateFileEx в зависимости от того, какие аргументы передаются.

А вот наш тривиальный тестовый код:

int main() {
  FileHandle bob("hello.txt");
  HANDLE value = *bob; // get the HANDLE out of the FileHandle
  bob.close(); // optional, to close early
}

Требования: ваши CloseFile должен принять Nuller()(std::declval<T&>()) и не плохо себя вести. По умолчанию Nuller()(...) просто присваивает ноль вашему T, который работает для многих типов ручек.

Он поддерживает семантику перемещения, что позволяет вам возвращать их из функции, но я не включил Copier аргумент (который, как я ожидаю, потребуется для любых объектов RAII, которые можно скопировать).

Живой пример с совсем немного другим кодом.

Другие вопросы по тегам