Форум программистов «Весельчак У»
  *
Добро пожаловать, Гость. Пожалуйста, войдите или зарегистрируйтесь.
Вам не пришло письмо с кодом активации?

  • Рекомендуем проверить настройки временной зоны в вашем профиле (страница "Внешний вид форума", пункт "Часовой пояс:").
  • У нас больше нет рассылок. Если вам приходят письма от наших бывших рассылок mail.ru и subscribe.ru, то знайте, что это не мы рассылаем.
   Начало  
Наши сайты
Помощь Поиск Календарь Почта Войти Регистрация  
 
Страниц: [1]   Вниз
  Печать  
Автор Тема: Обращение к элементам класса из внешних функций.  (Прочитано 3451 раз)
0 Пользователей и 1 Гость смотрят эту тему.
oktonion
Участник

ru
Offline Offline

« : 17-12-2013 16:01 » 

Доброго времени суток. Есть код и когда то понаписали исторически сложилось так что там есть класс, приватные элементы которого модифицирует внешняя функция. Логика примерно такова:
Сам класс
Код: (C++)
class TestClass
{
 private:
        int descriptor;
        void (*tc_func_p)(int a, int b, int &descr);
 public:
        TestClass(void);
        ~TestClass(void);
        void UseFunc(void);
};

Какие то функции из двух разных сторонних библиотек (как они атм внутри реализованы совершенно не известно), которые тем не менее имеют общую цель и почти не отличаются по аргументам.
Код: (C++)
void standart_func(int a, int b)//standart lib imp
{
 //some usefull code there
}

void alien_func(int a, int *b)//custom lib imp
{
 //some not so usefull code there
}

Реализация класса. Здесь написаны обертки для вышеприведенных сторонних функций, в одной из которых модифицируется приватная переменная descriptor.
Код: (C++)
//my wrappers
void wraped_standart_func(int a, int b, int &descr)
{
 return standart_func(a, b);
}

void wraped_alien_func(int a, int b, int &descr)
{
 descr = 5; //InitDescriptor(descr);

 return alien_func(a, &b);
}

TestClass::TestClass(void)
{
 this->descriptor = 0;

 #ifdef USE_ALIEN_FUNC
        this->tc_func_p = wraped_alien_func;
 #else
        this->tc_func_p = wraped_standart_func;
 #endif
}

TestClass::~TestClass(void)
{
 this->descriptor = 0;
}

void TestClass::UseFunc(void)
{
 this->tc_func_p(1, 2, this->descriptor);
}

Код: (C++)
#define USE_ALIEN_FUNC
Данный дефайн является индикатором того,  какая библиотека подключена и соответственно какую обертку и функцию использовать.

Ну и вызывается это все так:
Код: (C++)
int main(int argc, char* argv[])
{
 TestClass *tc_p = new TestClass();

 tc_p->UseFunc();

 delete tc_p;

 return 0;
}

Вопрос в следующем: корректно ли так делать? Является ли данный метод "костылем"? Если да, то как лучше это реализовать?

P.S.: Получилось немного сумбурно, за что заранее прошу прощения.
Записан
Dimka
Деятель
Команда клуба

ru
Offline Offline
Пол: Мужской

« Ответ #1 : 17-12-2013 17:00 » 

Если доверять твоим словам, то выходит шаблон проектирования "Посетитель" (Visitor). Соответственно, отлаженное и оптимальное решение в данном случае будет следовать шаблону.

Но если посмотреть на код, он словам не соответствует. А соответствует код совсем другой вещи: класс TestClass есть обёртка над функцией с сигнатурой tc_func_p. И обёртка эта используется для реализации шаблонов проектирования то ли "Команда" (Command), то ли "Посетитель" - в зависимости от того, что важнее: состояние обёрточного класса, которое меняется вызываемой функцией, или же инкапсуляция аргументов функции от пользователя и упаковка их в состояние объекта. Кроме того, по коду непонятна судьба аргументов a и b - они то встречаются, то не встречаются в коде. Если аргументы быть должны, то ещё и разновидность шаблона "Адаптер" (Adapter).

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


Резюмируя: вариантов сделать лучше масса, и это можно сделать разными способами. Но чтобы выбрать конкретный, нужно предметное знание задачи, а не абстрактное, как тобою изложено. Т.е. нужно понимать содержание переменных и характер работы вызываемых функций. Ты об этом ничего не сказал.

P.S. для модераторов: тему можно в "Технологии разработки" - тут больше про архитектуру, чем про C++.
« Последнее редактирование: 17-12-2013 17:04 от Dimka » Записан

Программировать - значит понимать (К. Нюгард)
Невывернутое лучше, чем вправленное (М. Аврелий)
Многие готовы скорее умереть, чем подумать (Б. Рассел)
oktonion
Участник

ru
Offline Offline

« Ответ #2 : 17-12-2013 20:01 » 

Dimka, спасибо за развернутый комментарий. Я постараюсь объяснить более конкретно что делает код:

Класс имеет намного большую функциональность чем просто работа с одной этой функцией, но он действительно скрывает от пользователя те механизмы, которые он вызывает для описанной мной в примере функциональности.
Есть две внешние библиотеки, с функциями, незначительно отличающимися решаемыми задачами, но с разным набором аргументов.
Есть класс, который вызывает эти функции (в зависимости от подключенной библиотеки) через указатель на функцию. Этому указателю присваивается (в зависимости, опять же, от подключенной библиотеки) адрес одной из двух функций-оберток, которые в свою очередь вызывают соответствующие функции из этих библиотек.
Отличие самих этих функций из библиотек в основном в том, что одна из них должна вызываться напрямую, а вторая в новом потоке. Идентификатор этого потока надо где то хранить, чтобы при вызове деструктора класса закрыть данный поток. Проблема в том, что идентификатор потока для каждого экземпляра класса должен быть свой и по-этому его хранят как раз в приватной переменной класса. А чтобы функция-обертка, которая не является членом класса могла получить доступ к этому идентификатору потока в прототип каждой функции-обертки (и указателя соответственно) добавлен еще один аргумент - как раз этот &descr.
На счет переменных a, b - в функции-обертки передаются приватные переменные из класса (в тестовом примере я их не указал, каюсь).

Возможно я что то снова упустил в объяснении, но надеюсь на то что задача данного кода стала более понятной.
Записан
Dimka
Деятель
Команда клуба

ru
Offline Offline
Пол: Мужской

« Ответ #3 : 17-12-2013 20:58 » 

Попытаюсь пересказать и решить задачу, основываясь на сказанном.

Итак, есть некая функция foo, которая имеет две разных реализации. Одна реализация - синхронная
Код: (C++)
void foo1(int a, int b);
Вторая реализация - асинхронная, построенная на типовом шаблоне begin-end.
Код: (C++)
Token foo2_begin(int a, int b);
void foo2_end(Token token);

Нужно сделать для пользователя общую синхронную функцию, в которой бы при компиляции выбирался тип реализации, и были бы скрыты все особенности этого зоопарка реализаций.

Решение в лоб:
Код: (C++)
void foo(int a, int b)
{
#if FOO=1
  foo1(a, b);
#else
  Token token = foo2_begin(a, b);
  foo2_end(token);
#endif
}

Чего я ещё не знаю, что это решение не подходит?
Записан

Программировать - значит понимать (К. Нюгард)
Невывернутое лучше, чем вправленное (М. Аврелий)
Многие готовы скорее умереть, чем подумать (Б. Рассел)
oktonion
Участник

ru
Offline Offline

« Ответ #4 : 18-12-2013 09:57 » 

Код: (C++)
  Token token = foo2_begin(a, b);

Код: (C++)
  foo2_end(token);

В данном случае Token это тот самый идентефикатор, как я понимаю, и есть инициализация и закрытие этого идентефикатора внутри функции-обертки. Дело только в том, что закрытие должно происходить не в этой же функции, а при вызове деструктора класса. Тоесть функция из сторонней библиотеки  запускается в новом потоке, и либо успевает завершиться до вызова деструктора и тогда поток "автоматически" закрывается, либо деструктор принудительно закрывает / убивает поток, так как эта функция уже больше не требуется.

Тоесть реализация ваша придет к такому виду:

Есть некая функция foo, которая имеет две разных реализации. Одна реализация - неблокирующая:
Код: (C++)
void foo1(int a, int b);

Вторая реализация блокирующая (плюс отличается набор входных аргументов, но это не так важно для примера):
Код: (C++)
void foo2(int a, int b);

Нужно сделать для пользователя общую неблокирующую функцию, в которой бы при компиляции выбирался тип реализации, и были бы скрыты все особенности этого зоопарка реализаций.

Код: (C++)
void ThreadFunc(struct_name args)
{
 foo2(args.a,  args.b);//waiting for this function to finish in this new thread
 ThreadID = 0;
}

void foo(int a, int b)
{
struct_name args;

args.a = a;
args.b = b;
#if FOO=1
  foo1(a, b);
#else
 if(!ThreadID)
  ThreadID = BeginThread(ThreadFunc, args);//begin foo2 function in new thread
#endif
}

TestClass::~TestClass(void)
{
 if(ThreadID)
   KillThread(ThreadID);
}

Проблема в том что ThreadID (как прочем и args) должен быть для каждого экземпляра класса свой, и должен быть доступен как внутри деструктора, так и в функции-обертке foo и в "поточной" функции.
« Последнее редактирование: 18-12-2013 09:58 от oktonion » Записан
Dimka
Деятель
Команда клуба

ru
Offline Offline
Пол: Мужской

« Ответ #5 : 18-12-2013 13:44 » 

Т.е. надо, наоборот, сделать асинхронный вызов.

Ну тогда задача разделяется на две:

1) Выбор одной из синхронных реализаций - обычная обёрточная функция
Код: (C++)
void foo(int a, int b)
{
#if FOO=1
  foo1(a, b);
#elif FOO=2
  foo2(a, b);
#else
  throw exception();
#endif
}

2) Реализация асинхронного вызова по шаблону begin-end. Общее оформление абстрактной работы делается по шаблону "Команда" (Command) маленьким модулем.

task.h файл, который видит клиент:
Код: (C++)
#include <exception>

using namespace std;

class Work
{
protected:
        virtual void _invoke() = 0;
public:
        class Token;
        static Work *invoke(Work *work);
        static Token *beginInvoke(Work *work) throw(exception);
        static Work *endInvoke(Token *token) throw(exception);
};
Основные положения:
1) Работа есть объект, аргументы и результаты работы которого спрятаны внутри и для механизма распараллеливания и синхронизации значения не имеют.
2) Работа попадает на вход, исполняется и оказывается на выходе, что конкретно она делает - скрыто внутри неё. Временем жизни объекта управляет пользователь.
3) Работа может выполняться синхронно - метод invoke.
4) Работа может выполняться асинхронно - методы beginInvoke, endInvoke. В этом случае идентификатором работы служит указатель типа Token, устройство которого пользователю знать не надо. Он обеспечивает привязку вызова endInvoke к ранее сделанному beginInvoke. На входе и на выходе по-прежнему один и тот же объект работы.
5) Обработка ошибок invoke не специфицирована: они могут быть, тогда объект работы назад не получим, их может не быть, тогда возвращается объект работы.
6) Обработка ошибок begin-end жёстко специфицирована. Либо работа запускается благополучно, либо по каким-то причинам это невозможно сделать, и тогда получаем исключение exception. Все прочие собственные ошибки работы в этой паре жёстко подавляются, и пользователь о них ничего не узнает (трансляция ошибок между потоками - это отдельная задача, которую мы здесь не решаем). Метод завершения работы никаких ошибок не возвращает, кроме той, что ему подсунули NULL вместо token.
7) Пользователю предлагается самостоятельно реализовать виртуальный метод _invoke, собственно выполняющий работу. Он не должен делать предположений о синхронном или асинхронном характере своего выполнения.

task.cpp файл реализации модуля с использованием WinAPI - системный пул потоков для асинхронных задач в оригинальном (WinNT) формате (в Win7/8 были усовершенствования - для обратной совместимости с WinXP и т.п. их не используем).
Код: (C++)
#include <Windows.h>

class Work::Token
{
private:

        HANDLE event;
        Work *work;

public:

        Token(Work *work) :
                event(NULL),
                work(NULL)
        {
                if(work == NULL)
                {
                        throw exception();
                }
                this->work = work;
                this->event = CreateEvent(NULL, TRUE, FALSE, NULL);
                if(this->event == NULL)
                {
                        throw exception();
                }
        }

        void begin()
        {
                BOOL result = QueueUserWorkItem(Work::Token::_work, this, 0);
                if(result == FALSE)
                {
                        throw exception();
                }
        }

        void invoke()
        {
                try
                {
                        Work::invoke(this->work);
                }
                catch(...)
                {
                        // Обязательный перехват всех исключений, чтобы не сломался механизм синхронизации.
                }
                SetEvent(this->event);
        }

        Work *end()
        {
                WaitForSingleObject(this->event, INFINITE);
                // В случае какой-либо ошибки ожидания всё равно ничего поделать не можем,
                // так что просто возвращаем пользователю объект работы - пусть сам разбирается,
                // насколько она удачно отработала.
                return this->work;
        }

        ~Token()
        {
                CloseHandle(this->event);
        }

        static DWORD WINAPI _work(LPVOID lpParameter)
        {
                if(lpParameter != NULL)
                {
                        try
                        {
                                static_cast<Work::Token *>(lpParameter)->invoke();
                        }
                        catch(...)
                        {
                                // Обязательный перехват всех исключений внутри потока, чтобы не разрушить системный пул потоков.
                        }
                }
                return S_OK;
        }
};

Work *Work::invoke(Work *work)
{
        if(work == NULL)
        {
                throw exception();
        }
        work->_invoke();
        return work;
}

Work::Token *Work::beginInvoke(Work *work)
{
        Work::Token *token = NULL;
        try
        {
                token = new Work::Token(work);
                token->begin();
        }
        catch(...)
        {
                if(token != NULL)
                {
                        delete token;
                }
                throw exception();
        }
        return token;
}

Work *Work::endInvoke(Token *token)
{
        if(token == NULL)
        {
                throw exception();
        }
        return token->end();
}
Заодно модуль прячет от пользователя тот факт, что использует WinAPI для реализации асинхронных вызовов. И Token тут используется как шаблон PImpl для сокрытия "потрохов" механизма синхронизации.

Наконец, этот полученный модуль может быть применён для решения прикладной задачи - вызова процедуры foo. Как известно, любая процедура имеет входные, выходные и входные-выходные параметры. В частности результат функции может рассматриваться как один из выходных параметров. Для общности примера сделаем такую функцию, которая что-то возвращает.
Код: (C++)
int foo(int a, int b);

class Foo :
        public Work
{
private:

        int a;
        int b;
        int result;

protected:

        virtual void _invoke()
        {
                this->result = foo(this->a, this->b);
        }

public:

        Foo() :
                a(0),
                b(0),
                result(0)
        {}

        void putA(const int a)
        {
                this->a = a;
        }

        void putB(const int b)
        {
                this->b = b;
        }

        int getResult() const
        {
                return this->result;
        }

        static Token *beginInvoke(int a, int b)
        {
                Foo *instance = new Foo();
                instance->putA(a);
                instance->putB(b);
                return Work::beginInvoke(instance);
        }

        static int endInvoke(Token *token)
        {
                Foo *instance = dynamic_cast<Foo *>(Work::endInvoke(token));
                if(instance == NULL)
                {
                        // Случай, когда подсунули "чужой" token с задачей другого типа.
                        throw exception();
                }
                int result = instance->getResult();
                delete instance;
                return result;
        }

        static int invoke(int a, int b)
        {
                Foo instance;
                instance.putA(a);
                instance.putB(b);
                Work::invoke(&instance);
                return instance.getResult();
        }

};
Общий подход такой:
- любой входной параметр x имеет метод установки значения putX,
- любой выходной параметр x имеет метод получения значения getX,
- любой входной-выходной параметр имеет, соответственно, оба метода,
- результат рассматривается как выходной параметр с именем result.

Для удобства использования объект самостоятельно реализует вспомогательные методы beginInvoke, endInvoke и invoke, которые являются обёрткой над вызываемой функцией. Метод invoke имеет в точности сигнатуру вызываемой функции. Метод beginInvoke содержит лишь входные и входные-выходные параметры и возвращает Token. Метод endInvoke принимает Token и содержит лишь выходные и входные-выходные параметры, а также возвращает результат.

И этим всем можно пользоваться - хоть синхронно:
Код: (C++)
        int a = 1, b = 2, c;
        c = Foo::invoke(a, b);
хоть асинхронно:
Код: (C++)
        int a = 1, b = 2, c;
        Work::Token *token = Foo::beginInvoke(a, b);
        c = Foo::endInvoke(token);

Главное во всём этом - систематическое применение вышеописанных правил оформления вызовов функций для решения всех подобных задач. И тогда внутри программы будет содержаться определённое архитектурное решение.

Если критична производительность, то вместо виртуального метода _invoke можно сооружать всякие template-конструкции, типа функторов стандартной библиотеки для <algorithm>.
« Последнее редактирование: 18-12-2013 15:25 от Dimka » Записан

Программировать - значит понимать (К. Нюгард)
Невывернутое лучше, чем вправленное (М. Аврелий)
Многие готовы скорее умереть, чем подумать (Б. Рассел)
oktonion
Участник

ru
Offline Offline

« Ответ #6 : 18-12-2013 14:48 » 

Модуль хорош, вы сделали львиную долю работы, но... я, наверное, зря не сказал с самого начала что работаю под QNX 6.5.
В любом случае спасибо, постараюсь разобраться конкретней и отпишусь, если появятся вопросы (а они, я чувствую, обязательно появятся).
Записан
Dimka
Деятель
Команда клуба

ru
Offline Offline
Пол: Мужской

« Ответ #7 : 18-12-2013 15:13 » 

oktonion, про QNX ничего не скажу. В C в общем-то есть своя реализация потоков, если не ошибаюсь - в process.h. Но, конечно, без пула. Т.е. каждый запуск функции потребует достаточно тяжёлого запуска потока. Или же свой пул вручную писать. Если не пользоваться никакими сторонними библиотеками.

Однако из WinAPI тут только сигнатура _work, флаг синхронизации и операция синхронизации на нём и запуск работы в пуле потоков. Так что заменить их на аналогичные по функциональности элементы из другой библиотеки проблемы не составляет. Это к архитектуре решения не относится.

В общем случае нужно:
- знать сигнатуру запускаемой в отдельном потоке функции, и соответствующим образом оформить _work;
- знать способ запуска _work в другом потоке;
- знать способ получить событие окончания работы: либо это завершение потока, либо какой-то объект синхронизации, который сам поток отпустит, когда закончит выполнение;
- знать способ, как остановить работу основного потока и ждать наступления события.

P.S. Я выше вызовы исправил: инстанцировать Foo при использовании не нужно, там статические методы.
« Последнее редактирование: 18-12-2013 15:22 от Dimka » Записан

Программировать - значит понимать (К. Нюгард)
Невывернутое лучше, чем вправленное (М. Аврелий)
Многие готовы скорее умереть, чем подумать (Б. Рассел)
Страниц: [1]   Вверх
  Печать  
 

Powered by SMF 1.1.21 | SMF © 2015, Simple Machines