Легенда:
новое сообщение
закрытая нитка
новое сообщение
в закрытой нитке
старое сообщение
|
- Напоминаю, что масса вопросов по функционированию форума снимается после прочтения его описания.
- Новичкам также крайне полезно ознакомиться с данным документом.
[C++] Получилось хорошее решение (но я испортил нитку в форуме). 19.12.07 07:50
Автор: void <Grebnev Valery> Статус: Elderman Отредактировано 28.12.07 08:17 Количество правок: 4
|
Прошу прощения, но я испортил нитку. Я хотел "ответить" на свой постинг в корень но каким -то образом затёр своё первое сообщение. Администраторы/модераторы - я не хотел..... ;( Если можно, то исправьте плз.
-----
В общем получилось весьма универсальное и производительное решение, см ниже.
Не map, а всё ж вектор. Работает быстро. Решение годится (протестировано) для 2-х типов задач:
- когда для обновлений добавляются относительно "статичные" топики, например, статусные переменные, результаты запросов к базам данных и т.д. Такие топики могут быть добавлены в вектор при помощи TopicVector::AddTopic и обновлены Subscriber-ом по таймеру.
- когда обновляются относительно "быстрые" топики 10000-15000 раз в секунду, например, данные для Microsoft RTD (real time data server). Не вдаваясь в обсуждение майкрософтовской трактовки "real time data", скажу, что данные накапливаются в TopicVector по приходу обновлений для топика при помощи TopicVector::UpdateTopic. Главный трюк - когда приходит обновление устанавливаем topic->m_cRef ++ , а когда Subscriber получает обновления из TopicVector, то он сбрасывает эти значения topic->m_cRef = 0.
Таким образом, вектор содержит единственное "вхождение" топика (как map), а данные всегда актуальны. Это в разы быстрее, чем map.
В принципе, по результатам тестов - решение весьма производительно. Если же надо будет быстрее, то, думаю, вектор можно будет заменить массивом (может и на отдельном хипе). Сделать это будет легко, поскольку для TopicVector не предусматривается операции удаления/вставки отдельного элемента, а только добаление в хвост и очистка всего вектора.
#include <string>
#include <vector>
#include "comdef.h"
const int INVALIDE_TOPIC_ID = -1;
const int TOPICVECTOR_RESERVE_SIZE = 500000;
typedef variant_t topic_t;
class Topic {
public:
virtual topic_t GetValue(void) const PURE;
Topic(long ID = INVALIDE_TOPIC_ID) : m_topicID(ID), m_cRef(0L) {};
virtual ~Topic(){};
long m_topicID;
long m_cRef;
};
class TopicVector
{
public:
void AddTopic(Topic* topic)
{
m_vector.push_back(topic);
topic->m_cRef ++;
}
void UpdateTopic(Topic* topic)
{
if ( 0 == topic->m_cRef)
m_vector.push_back(topic);
topic->m_cRef ++;
}
size_t Size(void) const
{
return m_vector.size();
}
bool IsValidTopic(const size_t idx) const
{
if (NULL != m_vector.at(idx) && INVALIDE_TOPIC_ID != m_vector.at(idx)->m_topicID )
return true;
else
return false;
}
Topic* operator[](const size_t idx)
{
return m_vector.at(idx);
}
void Lock(void)
{
::EnterCriticalSection(&m_cs);
}
void Unlock(void)
{
::LeaveCriticalSection(&m_cs);
}
void Clear(void)
{
m_vector.clear();
}
TopicVector()
{
::InitializeCriticalSection(&m_cs);
m_vector.reserve(TOPICVECTOR_RESERVE_SIZE);
}
~TopicVector()
{
::DeleteCriticalSection(&m_cs);
}
private:
std::vector<Topic*> m_vector;
CRITICAL_SECTION m_cs;
};
|
|
вот так было бы правильней, используя наследственность, ну... 20.12.07 02:13
Автор: + <Mikhail> Статус: Elderman
|
вот так было бы правильней, используя наследственность, ну естественно надо сделать private copy constructor и assignment operator.
Вообче то ето школьная задачка, не так ли?
class ITopic
{
public:
virtual ~ITopic(){};
virtual _bstr_t GetValue() = 0;
};
class CTopicVector
{
private:
//I would use a smart pointer
//example:
//map<int , smart_ptr<ITopic> > m_TopicData
map<int , ITopic*> m_TopicData;
CRITICAL_SECTION m_cs;
public:
CTopicVector()
{
InitializeCriticalSection(&m_cs);
};
virtual ~CTopicVector()
{
ClearVector();
DeleteCriticalSection(&m_cs);
};
void ClearVector()
{
map<int , ITopic*>::iterator it;
EnterCriticalSection(&m_cs);
try
{
for(it = m_TopicData.begin();it != m_TopicData.end();++it)
{
delete (*it).second;
}
m_TopicData.clear();
}catch(...){};
LeaveCriticalSection(&m_cs);
}
int GetSize()
{
return (int)m_TopicData.size();
}
int AddTopic(ITopic* pTopic)
{
int iId = (int)m_TopicData.size();
EnterCriticalSection(&m_cs);
try
{
m_TopicData[iId] = pTopic;
}catch(...){}
LeaveCriticalSection(&m_cs);
return iId;
};
//unsafe reference to an object
ITopic* GetUnsafeTopic(int iId)
{
ITopic *pTopic = NULL;
EnterCriticalSection(&m_cs);
try
{
pTopic = m_TopicData[iId];
}catch(...){}
LeaveCriticalSection(&m_cs);
return pTopic;
}
};
class CTopic1: public ITopic
{
private:
_bstr_t m_bstrVal;
int m_iVal;
public:
CTopic1(_bstr_t bstrVal, int iVal)
:m_bstrVal(bstrVal),m_iVal(iVal){}
_bstr_t GetValue()
{
return m_bstrVal + L":" + _bstr_t(_variant_t(m_iVal)) + L"\n";
}
};
class CTopic2: public ITopic
{
private:
double m_dVal;
bool m_bVal;
public:
CTopic2(double dVal, bool bVal)
:m_dVal(dVal),m_bVal(bVal){}
_bstr_t GetValue()
{
return _bstr_t(_variant_t(m_dVal)) + L":" + (m_bVal?L"true\n":L"false\n");
}
};
int _tmain(int argc, _TCHAR* argv[])
{
CTopicVector TopicVector;
TopicVector.AddTopic(new CTopic1(L"Topic1",1));
TopicVector.AddTopic(new CTopic1(L"Topic1",2));
TopicVector.AddTopic(new CTopic1(L"Topic1",3));
TopicVector.AddTopic(new CTopic2(3.14,true));
int size = TopicVector.GetSize();
for(int i = 0; i < size; ++i)
{
ITopic* pTopic = TopicVector.GetUnsafeTopic(i);
if(pTopic)
{
OutputDebugString(pTopic->GetValue());
}
}
return 0;
}
---
> Буду рад и благодарен, если кто-нить подскажет элегантное > решение. Задача такова: > Есть произвольные структуры данных (векторы, массивы и т.д) > classA {...} > classBB {...} > .... > > и есть векторы, массивы, мапы этих структур > > vector<A> va; > vector<BB> vb; > ... > > Каждый экземпляр таких структур/классов может > быть”отображён” на более простой тип, например, строку, > целое, булево и т.д. Т.е. Для каждого класса может быть > определена некоторая функция, типа virtual variant_t > GetTopicValue(const LPVOID pdata) const. > > > Существует поток, в котором надо единообразно периодически > “пробегать” по списку (контейнеру ссылок) всех этих > элементов, с тем чтобы в цикле получать разультат > “отображения” каждого экемпляра списка, например, в > псевдокоде надо выполнить: > > loop for each in (va,vb, ...) > v = each.GetTopicValue(NULL); > > Другие потоки добавляют в данный список ссылки на > существующие структуры, например, когда данные обновлены. > > Я смотрел аналогичный код некоторых гурей, у которых я > учусь– мне плохо стало от крутизны, непонятности, > темплейтности, объектно ориентированности и ненужности. > Попробовал сделать сам – получается просто, но хочется ещё > более просто, изящно и элегантно. Попробовал темлейты > прикрутить (как у гурей) – не получается чтоб это нужно > было (от недостатка моих знаний, видимо). Наколеночный код > у меня таков: > > #include "stdafx.h" > #include <string> > #include <vector> > #include <sstream> > #include "comdef.h" > > > /////////////////////////////////////////////////////////// > ////// > // > // Generic topic containers > // > /////////////////////////////////////////////////////////// > ////// > > interface ITopic { > virtual variant_t GetTopicValue(const LPVOID pdata) > const PURE; > virtual ~ITopic(){}; > }; > > class Topic { > public: > virtual variant_t GetTopicValue(const LPVOID pdata) > const > { > return (m_topicInterface ? > m_topicInterface->GetTopicValue(pdata) : variant_t()); > } > Topic() : m_topicID(0L), m_topicInterface(NULL) {} > long m_topicID; > ITopic* m_topicInterface; > }; > > class TopicVector > { > public: > void AddTopic(const long ID, ITopic* item) > { > Topic topic; > topic.m_topicID = ID; > topic.m_topicInterface = item; > m_vector.push_back(topic); > } > size_t Size(void) const > { > return m_vector.size(); > } > Topic& operator[](const size_t idx) > { > return m_vector.at(idx); > } > void Lock(void) > { > ::EnterCriticalSection(&m_cs); > } > void Unlock(void) > { > ::LeaveCriticalSection(&m_cs); > } > TopicVector() > { > ::InitializeCriticalSection(&m_cs); > } > ~TopicVector() > { > Lock(); > for (size_t i = 0; i < m_vector.size(); > i ++) > delete > m_vector.at(i).m_topicInterface; > Unlock(); > ::DeleteCriticalSection(&m_cs); > } > private: > std::vector<Topic> m_vector; > CRITICAL_SECTION m_cs; > }; > > /////////////////////////////////////////////////////////// > ////// > // > // Example: concrete topics > // > /////////////////////////////////////////////////////////// > ////// > > class ConcreteTopicA: public ITopic { > public: > virtual variant_t GetTopicValue(const LPVOID data) > const > { > std::ostringstream s; > s << m_sval << ":" << > m_ival; > return _bstr_t(s.str().c_str()); > } > ConcreteTopicA(const std::string& sval, const > int ival): m_sval(sval), m_ival(ival) {}; > virtual ~ConcreteTopicA() {}; > private: > std::string m_sval; > int m_ival; > }; > > class ConcreteTopicB: public ITopic { > public: > virtual variant_t GetTopicValue(const LPVOID data) > const > { > std::ostringstream s; > s << m_bval << ":" << > m_fval; > return _bstr_t(s.str().c_str()); > } > ConcreteTopicB(const bool bval, const double& > fval): m_bval(bval), m_fval(fval) {}; > virtual ~ConcreteTopicB() {}; > private: > bool m_bval; > double m_fval; > }; > > > int _tmain(int argc, _TCHAR* argv[]) > { > // create concrete topics > ConcreteTopicA* topicA = new ConcreteTopicA("some > string value", 100); > ConcreteTopicB* topicB = new ConcreteTopicB(true, > 1000.0); > > // add topics > TopicVector topics; > topics.Lock(); > > topics.AddTopic(1, topicA); > topics.AddTopic(2, topicB); > > //print it > variant_t v; > for (size_t i = 0; i < topics.Size(); i ++) { > v = topics[i].GetTopicValue(NULL); > // TO DO > } > > topics.Unlock(); > > return 0; > } > > Спасибо, если кто предложит красивое, простое решение.
|
| |
[C++] вот так было бы правильней, используя наследственность, ну... 21.12.07 04:40
Автор: void <Grebnev Valery> Статус: Elderman Отредактировано 21.12.07 04:42 Количество правок: 1
|
> вот так было бы правильней, используя наследственность, ну > естественно надо сделать private copy constructor и > assignment operator. > Вообче то ето школьная задачка, не так ли?
Так :(... Я бы даже сказал детсадовская... Но тем неменее могут быть и шероховатости!
> class ITopic > { > public: > virtual ~ITopic(){}; > virtual _bstr_t GetValue() = 0; > };
Я уберу virtual в определении деструктора ~ITopic. Эти указатели указывают на структуры данных, которые должны постоянно находиться в памяти и обновляться, возможно, из других потоков по наступлении некоторых событий.
> class CTopicVector > { > private: > //I would use a smart pointer
Написал выше, что это нецелесообразно.
> //example: > //map<int , smart_ptr<ITopic> > m_TopicData > map<int , ITopic*> m_TopicData;
Возможно и не обойтись без map<int , ITopic*> vs vectior<Topic>. Но ... при использовании std::map мы можем сильно проиграть в производительности при большом числе элементов(~100 000) CTopicVector.
Ещё раз скажу о постановке задачи:
1)В начале цикла обработке CTopicVector::Size() == 0.
2) При поступлении обновлений данных, клиенты будут добавлять элементы при помощи AddTopic(ITopic* pTopic) в CTopicVector с указателями на свои структуры данных. Таким образом размер CTopicVector будет расти от нуля до некоторого значения N.
3)В некоторый момент (по таймеру, или условию N == MAX(N)) CTopicVector должен быть “flush” данные в некий процесс, и затем очищен. Функция, что ниже должна работать максимально быстро, чтобы не держать клиентов, обновляющих данные(ITopic, на некотором объекте синхронизации Lock()/Unlock():
STDMETHODIMP SomeObject::RefreshData( long *TopicCount,
SAFEARRAY **parrayOut)
{
...
// нечто вроде
Lock();
> int size = TopicVector.GetSize(); > for(int i = 0; i < size; ++i) > { > ITopic* pTopic = TopicVector.GetUnsafeTopic(i); > if(pTopic) > { > OutputDebugString(pTopic->GetValue()); > } > } ...
TopicVector.ClearVector()
Unlock();
}
Класс map куда более тяжелее в сравнении с vector при большом числе элементов (~100000) и при частом добавлении/удалении узлов. Напротив, для вектора можно “преаллокировать” сapacity(), так что освобождение вектора не будет приводить к фактической переаллокации памяти на глобальном хипе. В принципе, можно пойти дальше – создать массив вместо вектора в отдельном хипе. Пока не уверен, что это даст прирост производительности. Можно будет протестировать. И последнее, при интенсивных циклах создания/освобождения элементов TopicVector, при использовании vector или array не будет фрагментации памяти.
> CRITICAL_SECTION m_cs; > > public: > CTopicVector() > { > InitializeCriticalSection(&m_cs); > }; > virtual ~CTopicVector() > { > ClearVector(); > DeleteCriticalSection(&m_cs); > }; > > void ClearVector() > { > map<int , ITopic*>::iterator it; > EnterCriticalSection(&m_cs);
Не пойдёт, см. ниже.
> try > { > for(it = m_TopicData.begin();it != > m_TopicData.end();++it) > { > delete (*it).second; > } > m_TopicData.clear(); > }catch(...){}; > LeaveCriticalSection(&m_cs); > } > > int GetSize() > { > return (int)m_TopicData.size(); > } > int AddTopic(ITopic* pTopic) > { > int iId = (int)m_TopicData.size(); > EnterCriticalSection(&m_cs);
Не пойдёт, см. ниже.
> try > { > m_TopicData[iId] = pTopic; > }catch(...){} > LeaveCriticalSection(&m_cs); > return iId; > }; > //unsafe reference to an object > ITopic* GetUnsafeTopic(int iId) > { > ITopic *pTopic = NULL; > EnterCriticalSection(&m_cs); > try > { > pTopic = m_TopicData[iId]; > }catch(...){} > LeaveCriticalSection(&m_cs); > return pTopic; > } > > };
Указанное выше “ниже” - это то, что блокировка должна охватывать весь процесс SomeObject::RefreshData. Иначе, данные CTopicVector будут неактуальными или испорченными другими клиентскими потоками, которые ” в этот момент” пытаются обновить свои структуры(ITopic. Даже если и не придавать значения такой “рассинхронизации” (не гут), то некоторые обновления и вовсе могут быть потеряны, т.к. по выходу из SomeObject::RefreshData вектор CtopicVector очищается. Поэтому необходимы отдельные методы Lock()/Unlock(). Другая причина в том, что вызов EnterCriticalSection – это не бесплатный вызов, даже если объект не блокируется (если критическая секция не занята, то на EnterCriticalSection может приходится ~ 100 тактов ЦПУ). Ну а если, уж объект синхнонизации блокирован, то дело совсем плохо. Поэтому нельзя, на мой взгляд, использовать такие Lock-и внутри циклов. Надо делать это снаружи циклов.
И последняя шероховатость, которая не относится к эффективности, но к элегантности:
и у Вас и у Нас есть по-существу GetUnsafeTopic, как Вы справедливо заметили . Ключевое слово здесь Unsafe, и я не знаю что с этим делать. Похоже этой неэлегантной шероховатости не избежать, так чтоб не родить другую неэлегантность.
В целом я очень признателен Вам, даже несмотря на шероховатости обсуждаемых решений. Это придало мне уверенности, что тот код (не мой), который мне надо адаптировать – в топку и целиком.
|
| | |
Нууу, брат ты погнал, "Я уберу virtual в определении... 21.12.07 21:05
Автор: + <Mikhail> Статус: Elderman
|
> > вот так было бы правильней, используя > наследственность, ну > > естественно надо сделать private copy constructor и > > assignment operator. > > Вообче то ето школьная задачка, не так ли? > > Так :(... Я бы даже сказал детсадовская... Но тем неменее > могут быть и шероховатости! > > > class ITopic > > { > > public: > > virtual ~ITopic(){}; > > virtual _bstr_t GetValue() = 0; > > }; > > Я уберу virtual в определении деструктора ~ITopic. Нууу, брат ты погнал, "Я уберу virtual в определении деструктора ~ITopic"
сотри немедленно и считай что я не слышал :).
>Эти
> указатели указывают на структуры данных, которые должны > постоянно находиться в памяти и обновляться, возможно, из > других потоков по наступлении некоторых событий. > > > > class CTopicVector > > { > > private: > > //I would use a smart pointer > > Написал выше, что это нецелесообразно. > > > > //example: > > //map<int , smart_ptr<ITopic> > > m_TopicData > > map<int , ITopic*> m_TopicData; > > Возможно и не обойтись без map<int , ITopic*> vs > vectior<Topic>. Но ... при использовании std::map мы > можем сильно проиграть в производительности при большом > числе элементов(~100 000) CTopicVector. > Ещё раз скажу о постановке задачи: > > 1)В начале цикла обработке CTopicVector::Size() == 0. > 2) При поступлении обновлений данных, клиенты будут > добавлять элементы при помощи AddTopic(ITopic* pTopic) в > CTopicVector с указателями на свои структуры данных. Таким > образом размер CTopicVector будет расти от нуля до > некоторого значения N. > 3)В некоторый момент (по таймеру, или условию N == MAX(N)) > CTopicVector должен быть “flush” данные в некий процесс, и > затем очищен. Функция, что ниже должна работать максимально > быстро, чтобы не держать клиентов, обновляющих данные >(ITopic, на некотором объекте синхронизации
> Lock()/Unlock(): > > STDMETHODIMP SomeObject::RefreshData( long *TopicCount, > SAFEARRAY **parrayOut) > { > > ... > // нечто вроде > Lock(); > > int size = TopicVector.GetSize(); > > for(int i = 0; i < size; ++i) > > { > > ITopic* pTopic = TopicVector.GetUnsafeTopic(i); > > if(pTopic) > > { > > OutputDebugString(pTopic->GetValue()); > > } > > } > ... > TopicVector.ClearVector() > Unlock(); > } > > Класс map куда более тяжелее в сравнении с vector при > большом числе элементов (~100000) и при частом
Про количество елементов ты только сейчас заикнулся :)
> добавлении/удалении узлов. Напротив, для вектора можно > “преаллокировать” сapacity(), так что освобождение вектора > не будет приводить к фактической переаллокации памяти на > глобальном хипе. В принципе, можно пойти дальше – создать > массив вместо вектора в отдельном хипе. Пока не уверен, что > это даст прирост производительности. Можно будет > протестировать. И последнее, при интенсивных циклах > создания/освобождения элементов TopicVector, при > использовании vector или array не будет фрагментации > памяти.
Фрагментация будет по любому, если ты конечно не преаллоцируешь память для твоих Topic objects
> > > > CRITICAL_SECTION m_cs; > > > > public: > > CTopicVector() > > { > > InitializeCriticalSection(&m_cs); > > }; > > virtual ~CTopicVector() > > { > > ClearVector(); > > DeleteCriticalSection(&m_cs); > > }; > > > > void ClearVector() > > { > > map<int , ITopic*>::iterator it; > > EnterCriticalSection(&m_cs); > > > Не пойдёт, см. ниже. > > > > try > > { > > for(it = m_TopicData.begin();it != > > m_TopicData.end();++it) > > { > > delete (*it).second; > > } > > m_TopicData.clear(); > > }catch(...){}; > > LeaveCriticalSection(&m_cs); > > } > > > > int GetSize() > > { > > return (int)m_TopicData.size(); > > } > > int AddTopic(ITopic* pTopic) > > { > > int iId = (int)m_TopicData.size(); > > EnterCriticalSection(&m_cs); > > > Не пойдёт, см. ниже. > > > > try > > { > > m_TopicData[iId] = pTopic; > > }catch(...){} > > LeaveCriticalSection(&m_cs); > > return iId; > > }; > > //unsafe reference to an object > > ITopic* GetUnsafeTopic(int iId) > > { > > ITopic *pTopic = NULL; > > EnterCriticalSection(&m_cs); > > try > > { > > pTopic = m_TopicData[iId]; > > }catch(...){} > > LeaveCriticalSection(&m_cs); > > return pTopic; > > } > > > > }; > > > Указанное выше “ниже” - это то, что блокировка должна > охватывать весь процесс SomeObject::RefreshData. Иначе, > данные CTopicVector будут неактуальными или испорченными > другими клиентскими потоками, которые ” в этот момент” > пытаются обновить свои структуры(ITopic. Даже если и не
поетому smart poiter и нужен
> придавать значения такой “рассинхронизации” (не гут), то > некоторые обновления и вовсе могут быть потеряны, т.к. по > выходу из SomeObject::RefreshData вектор CtopicVector > очищается. Поэтому необходимы отдельные методы > Lock()/Unlock(). Другая причина в том, что вызов > EnterCriticalSection – это не бесплатный вызов, даже если > объект не блокируется (если критическая секция не занята, > то на EnterCriticalSection может приходится ~ 100 тактов > ЦПУ). Ну а если, уж объект синхнонизации блокирован, то > дело совсем плохо. Поэтому нельзя, на мой взгляд, > использовать такие Lock-и внутри циклов. Надо делать это > снаружи циклов. > И последняя шероховатость, которая не относится к > эффективности, но к элегантности: > и у Вас и у Нас есть по-существу GetUnsafeTopic, как Вы > справедливо заметили . Ключевое слово здесь Unsafe, и я не > знаю что с этим делать. Похоже этой неэлегантной > шероховатости не избежать, так чтоб не родить другую > неэлегантность. >
поетому smart poiter и нужен
> В целом я очень признателен Вам, даже несмотря на > шероховатости обсуждаемых решений. Это придало мне > уверенности, что тот код (не мой), который мне надо > адаптировать – в топку и целиком.
показал бы ТОТ код , который "не нужен"
|
| | | |
Согласен, что погнал, но не только здесь 23.12.07 00:22
Автор: void <Grebnev Valery> Статус: Elderman
|
> > Я уберу virtual в определении деструктора ~ITopic. > Нууу, брат ты погнал, "Я уберу virtual в определении > деструктора ~ITopic" > сотри немедленно и считай что я не слышал :).
Я попробовал встоить код в существующий солюшен... Не работает.
> показал бы ТОТ код , который "не нужен"
Не могу. То собственность комании. Там активно темплейты используются.
|
| | |
А вот ещё мелькнуло... Может вообще не освобождать вектор в... 21.12.07 05:11
Автор: void <Grebnev Valery> Статус: Elderman
|
А вот ещё мелькнуло... Может вообще не освобождать вектор в методе RefreshData. Тогда, можно будет использовать map, и дополнительно устанавливать флаг = 1 если данные были обновлены клиентом (флаг = 1 устанавливается клиентом), и сбрасывать флаг в 0 в методе RefreshData.
Единственный недостаток пока вижу, это то, что если клиент перестал обновлять топик, (отсоединился), то соответствующуб ссылку надо удалить. Впрочем, это будет случаться не очень часто.
???
> STDMETHODIMP SomeObject::RefreshData( long *TopicCount, > SAFEARRAY **parrayOut) > { > > ... > // нечто вроде > Lock(); > > int size = TopicVector.GetSize(); > > for(int i = 0; i < size; ++i) > > { > > ITopic* pTopic = TopicVector.GetUnsafeTopic(i); > > if(pTopic) > > { > > OutputDebugString(pTopic->GetValue()); > > } > > } > ... > TopicVector.ClearVector() > Unlock(); > }
|
|
|