Допустим мы хотим обернуть вызовы методов класса, что бы перед вызовом класс автоматически "блокировался", а после вызова "разблокировался". Распространенная практика.
Один подход заключается в том, что бы operator->() объекта возвращал некий прокси-объект, который в конструкторе блокирует объект, а в деструкторе разблокирует. Тут не нравится то, что прокси-объект должен быть копируемым. Т.е. надо возиться с передачей владения, и он не может содержать не копируемые вложенные объекты (boost::detail::lightweight_mutex::scoped_lock).
Второй подход заключается в том, что бы стеке руками создавать вспомогательный объект (с использованием временного объекта):
scoped_lock(x)->f();
Тут scoped_lock может (должен) быть не копируемым. Хорошо. Но тут встаёт другая проблема — тип scoped_lock должен быть свой для каждого типа пользовательского объект. Т.е. если мы хотим реализовать его в виде повторно-используемого компонента, то это будет выглядеть так:
scoped_lock<X>(x)->f();
scoped_lock<Y>(y)->g();
Не очень красиво с т.з. пользователя.
Собственно что придумалось по этому поводу.
Допустим есть 2 таких несвязанных класса:
struct X
{
void f(){}
boost::detail::lightweight_mutex mtx_;
};
struct Y
{
void g(){}
boost::detail::lightweight_mutex mtx_;
};
И мы хотим, что бы можно было писать вот так:
int main()
{
X x;
lock(x)->f();
Y y;
lock(y)->g();
}
Во-первых, я отделил прокси-объект от самого пользовательского объекта. Т.е. они не связаны по типам, и никто не содержит ни на кого ссылок.
Во-вторых, я отделил создание "стораджа" для прокси-объекта, от создания самого прокси-объекта. Сторадж создается на стеке в месте вызова lock(), а прокси-объект внутри функции lock(). Рушится прокси-объект в конце полного выражения в месте вызова.
По-моему, получилось достаточно интересное по свойствам решение.
При необходимости можно сделать прокси-объект шаблонным (зависимым от типа пользовательского объекта):
Здравствуйте, Bell, Вы писали:
B>Здравствуйте, remark, Вы писали:
B>Мне кажется, необходимость наличия в защищаемом классе открытого члена mtx_ несколько огланичивает область применения такой обертки.
Обёртку своего класса можно сделать своим другом...
Все эмоциональные формулировки не соотвествуют действительному положению вещей и приведены мной исключительно "ради красного словца". За корректными формулировками и неискажённым изложением идей, следует обращаться к их автором или воспользоваться поиском
Здравствуйте, Erop, Вы писали:
E>Здравствуйте, Bell, Вы писали:
B>>Здравствуйте, remark, Вы писали:
B>>Мне кажется, необходимость наличия в защищаемом классе открытого члена mtx_ несколько огланичивает область применения такой обертки.
E>Обёртку своего класса можно сделать своим другом...
Если речь идет только о своих классах — то да, можно, спору нет.
Здравствуйте, Bell, Вы писали:
B>Здравствуйте, Erop, Вы писали:
E>>Здравствуйте, Bell, Вы писали:
B>>>Здравствуйте, remark, Вы писали:
B>>>Мне кажется, необходимость наличия в защищаемом классе открытого члена mtx_ несколько огланичивает область применения такой обертки.
E>>Обёртку своего класса можно сделать своим другом...
B>Если речь идет только о своих классах — то да, можно, спору нет.
Непосредственно открытый мьюткс с одним названием я взял для простоты изложения.
Каким-либо образом пользовательский класс всё равно должен поддерживать эту функциональность, т.к. он должен содержать мьютекс.
Более приемлемо это можно записать так:
class sync
{
boost::mutex mtx_;
friend ...;
};
struct X : sync
{
// ...
};
struct Y : sync
{
// ...
};
Учитывая минимальный размер функции lock() не составляет труда просто "скопи-пастить" её под разные варианты.
И я не вижу причин, мешающих применению такого приёма не "своих" классов, главное — что бы класс поддерживал возможность блокировки/разблокировки в каком-либо виде.
Здравствуйте, Andrew S, Вы писали:
AS>>>>Ну, тоже вариант, в принципе. Может, даже более интересный за счет явного указания намерений. Вот только клиенту придется явно указывать тип этого прокси, ведь порождающую функцию тут применить нельзя по тем же самым причинам. А хотелось бы избежать лишних телодвижений. Библиотечный код пишется один раз, а вот пользуются им гораздо чаще
R>>>Надо подумать...
R>>http://gzip.rsdn.ru/forum/message/2937445.1.aspx
AS>Да, спасибо, посмотрел. Интересное решение, в том плане, что кажется, будто это снимает требование наличия конструктора копирования у временного объекта. На самом деле, все остается на том же уровне — ведь scoped_lock _должен_ быть сконструирован, т.е. конструктора по умолчанию у стораджа быть просто не может.
Нет! Конструктор по-умолчанию boost::optional вообще не вызывает конструктор пользовательского класса, он просто содержит неинициализированный сторадж под класс.
AS>Т.е. сам подход к конструированию интересный, но он не применим в данном конкретном случае.
Именно для этого случая я его закодировал и скомпилировал.
AS>Другой вопрос касаемо этого подхода (там, где он применим) — сможет ли оптимизатор так же хорошо разобраться во всей этой кухне, как и с RVO — это надо смотреть...
Вот набросок варианта для "нелюбителей boost'а", из которого видно, что оверхеда-то тут особо и нет:
Здравствуйте, remark, Вы писали:
R>Во-первых, я отделил прокси-объект от самого пользовательского объекта. Т.е. они не связаны по типам, и никто не содержит ни на кого ссылок. R>Во-вторых, я отделил создание "стораджа" для прокси-объекта, от создания самого прокси-объекта. Сторадж создается на стеке в месте вызова lock(), а прокси-объект внутри функции lock(). Рушится прокси-объект в конце полного выражения в месте вызова.
R>По-моему, получилось достаточно интересное по свойствам решение.
Причём этот подход можно обобщить на любые ситуации с
— возвращением некопируемого временного объекта
Но увы, это не компилируется. Поэтому и пришлось задействовать отложенное конструирование.
Да, кстати!
Зачем сигнатура T* lock(T&) ? Только ради ->, или это плоды рефакторинга?
Если отвлечься от ->, то пускай она будет T& lock(T&), и использовать
Здравствуйте, Кодт, Вы писали:
К>Здравствуйте, remark, Вы писали:
R>>Во-первых, я отделил прокси-объект от самого пользовательского объекта. Т.е. они не связаны по типам, и никто не содержит ни на кого ссылок. R>>Во-вторых, я отделил создание "стораджа" для прокси-объекта, от создания самого прокси-объекта. Сторадж создается на стеке в месте вызова lock(), а прокси-объект внутри функции lock(). Рушится прокси-объект в конце полного выражения в месте вызова.
R>>По-моему, получилось достаточно интересное по свойствам решение.
К>Причём этот подход можно обобщить на любые ситуации с К>- возвращением некопируемого временного объекта
Вполне. Только надо учитывать, что этот объект можно использовать только "в пределах одной строки".
Для интереса можно даже так:
К>
template<size_t sz>
struct buf_t
{
mutable char data [sz];
};
char* foo(buf_t<64> const& buf = buf_t<64>())
{
// отъели немного стека во фрейме вызывающей функции
// ...return (char*)buf;
}
О! Самое интересное так:
char* foo(void* buf = alloca(64))
{
// отъели немного стека во фрейме вызывающей функции
// память будет жить до конца вызывающей функцииreturn strcpy((char*)buf, "Hello from dead!");
}
int main()
{
char* str = foo();
printf("%s", str);
// магия: str никаким образом удалять не надо - вся строка у нас на стеке
}
Хммм... вроде это законно, т.к. вычисление аргументов по-умолчанию синтаксически есть часть вызывающей функции.
По крайней мере в MSVC никакие ран-тайм проверки целостности стека не срабатывают.
К>что примерно соответствует встроенному оператору ',' (только тщательно спрятанному) К>
К>(scoped_lock(x.mtx_), x).member();
К>
"(scoped_lock(x.mtx_), x)" не рулит. Изначально у меня был такой вариант "lock()(x)", где lock не шаблонный класс, но operator() у него шаблонный. Но лишние скобочки сильно смущали.
К>Но увы, это не компилируется. Поэтому и пришлось задействовать отложенное конструирование.
Ну, да. Это первое, что я написал
К>Да, кстати! К>Зачем сигнатура T* lock(T&) ? Только ради ->, или это плоды рефакторинга? К>Если отвлечься от ->, то пускай она будет T& lock(T&), и использовать К>
К>lock(x).member();
К>
Да, лучше заменить на ссылку. Указатель — исторически.
Ещё одно приемущество
Переименовать lock в synchronized и прям Java получается
Здравствуйте, remark, Вы писали:
R>Учитывая минимальный размер функции lock() не составляет труда просто "скопи-пастить" её под разные варианты. R>И я не вижу причин, мешающих применению такого приёма не "своих" классов, главное — что бы класс поддерживал возможность блокировки/разблокировки в каком-либо виде.
Есть и третий вариант, вообще-то.
Можно завести две функции, ну типа BeginLockedAccess( T& ) и EndLockedAccess( T& ), и звать из твоей функции эти враперы, для того, чтобы начать и закончить.
Ну а для разных параметров определить разные функции просто...
Все эмоциональные формулировки не соотвествуют действительному положению вещей и приведены мной исключительно "ради красного словца". За корректными формулировками и неискажённым изложением идей, следует обращаться к их автором или воспользоваться поиском
R>Нет! Конструктор по-умолчанию boost::optional вообще не вызывает конструктор пользовательского класса, он просто содержит неинициализированный сторадж под класс.
Ну так а чем тогда все это отличается от того вариант 2, что предложил я? Фактически, получается то же самое, разница только в том, где конструируется объект...
Здравствуйте, Andrew S, Вы писали:
R>>Нет! Конструктор по-умолчанию boost::optional вообще не вызывает конструктор пользовательского класса, он просто содержит неинициализированный сторадж под класс.
AS>Ну так а чем тогда все это отличается от того вариант 2, что предложил я? Фактически, получается то же самое, разница только в том, где конструируется объект...
Ну твой вариант 2 — это вообще неработающий хак...
От варианта 1 отличается тем, что будет работать и без RVO, и пользователь не попадёт в ситуацию, когда ему линкер вдруг выдаёт непонятную ошибку. А когда пользователь поймёт, что за ошибка, то исправлять её не понятно как... если он, конечно, не читал топик "[Trick] Call wrapper"
Здравствуйте, Erop, Вы писали:
E>Здравствуйте, remark, Вы писали:
R>>Учитывая минимальный размер функции lock() не составляет труда просто "скопи-пастить" её под разные варианты. R>>И я не вижу причин, мешающих применению такого приёма не "своих" классов, главное — что бы класс поддерживал возможность блокировки/разблокировки в каком-либо виде.
E>Есть и третий вариант, вообще-то. E>Можно завести две функции, ну типа BeginLockedAccess( T& ) и EndLockedAccess( T& ), и звать из твоей функции эти враперы, для того, чтобы начать и закончить. E>Ну а для разных параметров определить разные функции просто...
Мммм... а состояние-то где хранить. Допустим BeginLockedAccess() хочет запомнить идентификатор потока, что бы EndLockedAccess() потом мог его сверить.
R>>>Нет! Конструктор по-умолчанию boost::optional вообще не вызывает конструктор пользовательского класса, он просто содержит неинициализированный сторадж под класс.
AS>>Ну так а чем тогда все это отличается от того вариант 2, что предложил я? Фактически, получается то же самое, разница только в том, где конструируется объект...
R>Ну твой вариант 2 — это вообще неработающий хак...
Ровно аналогичный код с boost::optional. Все там работает, ну, можно еще алигмент поправить, если, конечно, для ссылки это важно (в чем я очень сомневаюсь).
R>От варианта 1 отличается тем, что будет работать и без RVO, и пользователь не попадёт в ситуацию, когда ему линкер вдруг выдаёт непонятную ошибку. А когда пользователь поймёт, что за ошибка, то исправлять её не понятно как... если он, конечно, не читал топик "[Trick] Call wrapper"
Проблема в том, что заставить "просто так" работать этот lock на скоуп довольно сложно — надо передавать свой сторадж, а значит, клиенту опять знать тип объекта. Вариант с RVO можно просто отрастить от пустой базы и присваивать константной ссылке, которая будет держать нужное время.
В общем, лично я остановился на варианте с RVO, он мне кажется более понятным как в реализации, так и по побочным эффектам...
Если интересно, могу запостить результат.
Открытый член класса, да с именем, подразумевающим, что член закрыт (_ на конце) — отличное наследство несчастым, которым "повезет" сапортить этот код.
Можно конечно сделать функцию T* lock(...) другом. Но ее объявление в теле класса будет выглядеть довольно уродливо:
class A
{
mutex mtx_;
friend A* lock<>(A& obj, lock_storage const&);
};
Да и толку-то от такого решения никакого — вы не закрыли возможность дернуть методы класса A без предварительной блокировки экземпляра.
Здравствуйте, Andrew S, Вы писали:
R>>>>Нет! Конструктор по-умолчанию boost::optional вообще не вызывает конструктор пользовательского класса, он просто содержит неинициализированный сторадж под класс.
AS>>>Ну так а чем тогда все это отличается от того вариант 2, что предложил я? Фактически, получается то же самое, разница только в том, где конструируется объект...
R>>Ну твой вариант 2 — это вообще неработающий хак...
AS>Ровно аналогичный код с boost::optional.
Подожди-подожди. Я memcpy() объекты не копирую! У меня с т.з. языка всё законно.
R>>Все там работает,
А ты попробуй в объект добавить указатель внутрь себя. И потом погляди куда он будет указывать после memcpy() (если более точно, то там у тебя не memcpy(), а копирование встроенного в объект массива, но по сути это то же самое).
R>>ну, можно еще алигмент поправить, если, конечно, для ссылки это важно (в чем я очень сомневаюсь).
Ссылка в данном случае -- указатель. На Alpha словишь аппаратное прерывание. На x86 тоже словишь, если включена аппаратная проверка выравнивания.
R>>От варианта 1 отличается тем, что будет работать и без RVO, и пользователь не попадёт в ситуацию, когда ему линкер вдруг выдаёт непонятную ошибку. А когда пользователь поймёт, что за ошибка, то исправлять её не понятно как... если он, конечно, не читал топик "[Trick] Call wrapper"
AS>Проблема в том, что заставить "просто так" работать этот lock на скоуп довольно сложно — надо передавать свой сторадж, а значит, клиенту опять знать тип объекта.
Тип стораджа *не* зависит от типа пользовательского объекта. В любом случае, что страшного в этом знании как таковом?
AS>Вариант с RVO можно просто отрастить от пустой базы и присваивать константной ссылке, которая будет держать нужное время.
AS>В общем, лично я остановился на варианте с RVO, он мне кажется более понятным как в реализации, так и по побочным эффектам... AS>Если интересно, могу запостить результат.
Здравствуйте, remark, Вы писали:
R>Непосредственно открытый мьюткс с одним названием я взял для простоты изложения.
Принято.
R>Каким-либо образом пользовательский класс всё равно должен поддерживать эту функциональность, т.к. он должен содержать мьютекс.
Принято.
R>Более приемлемо это можно записать так:
R>
R>Учитывая минимальный размер функции lock() не составляет труда просто "скопи-пастить" её под разные варианты. R>И я не вижу причин, мешающих применению такого приёма не "своих" классов, главное — что бы класс поддерживал возможность блокировки/разблокировки в каком-либо виде.
И тем не менее, я не вижу особых преимуществ перед записью
R>>>Ну твой вариант 2 — это вообще неработающий хак...
AS>>Ровно аналогичный код с boost::optional.
R>Подожди-подожди. Я memcpy() объекты не копирую! У меня с т.з. языка всё законно.
Там не объект, а _ссылка_.
R>>>Все там работает,
R>А ты попробуй в объект добавить указатель внутрь себя. И потом погляди куда он будет указывать после memcpy() (если более точно, то там у тебя не memcpy(), а копирование встроенного в объект массива, но по сути это то же самое).
Так специально и сделан объект, а не ручками мувится. Все, что там надо добавить — алигн, как это сделано в бусте.
R>>>ну, можно еще алигмент поправить, если, конечно, для ссылки это важно (в чем я очень сомневаюсь).
R>Ссылка в данном случае -- указатель. На Alpha словишь аппаратное прерывание. На x86 тоже словишь, если включена аппаратная проверка выравнивания.
С чего бы это вдруг ссылка стала равна указателю? В данном случае максимум, что надо, макс алигн добавить. А вообще, как я уже говорил, даже 6-ка сворачивает этот код в полностью inline, никакого там копирования нет и не будет в прицнипе Эта реализация фактически остается для совсем дубовых компиляторов.
R>>>От варианта 1 отличается тем, что будет работать и без RVO, и пользователь не попадёт в ситуацию, когда ему линкер вдруг выдаёт непонятную ошибку. А когда пользователь поймёт, что за ошибка, то исправлять её не понятно как... если он, конечно, не читал топик "[Trick] Call wrapper"
AS>>Проблема в том, что заставить "просто так" работать этот lock на скоуп довольно сложно — надо передавать свой сторадж, а значит, клиенту опять знать тип объекта.
R>Тип стораджа *не* зависит от типа пользовательского объекта. В любом случае, что страшного в этом знании как таковом?
В том, что его (знание) надо ИМЕТЬ. Что клиенту бывает довольно неудобно. Например, мне гораздо удобнее написать const Lock &tmp = lock(obj), чем пытаться вспомнить, что там за тип надо отдать в lock (смысл такой операции, кстати, клиенту может быть совсем не очевиден). Везде свои плюсы и минусы...
Здравствуйте, Andrew S, Вы писали:
T>>Таким методом можно проэмулировать динамические массивы на стеке, которые умеет gcc:
AS>А теперь представим, что массив аллоцируют в цикле... Лучше так не эмулировать
alloca — это вообще страшная вещь.
Помимо того, что можно запросить слишком много, если не проверять размер. И помимио того, что нельзя выделять в цикле.
Самая прикольная засада то, что и такая функция может стать причиной переполнения стека:
void f()
{
alloca(1);
}
При условии, что она вызывается так:
void g()
{
for (int i = 0; i != 1000000; ++i)
f();
}
И f() встроится в g(). Тогда тот самый "конец функции", в котором удаляется память, выделенная alloca(1), переносится в конец функции g(). Т.е. память выделяется, выделяется, выделяется... Всё вышеописанное относится к msvc, на gcc не проверял.
Лечится только объявлением всех функций, которые используют alloca, как __declspec(noinline).