176 / 2 / 1
Регистрация: 31.10.2016
Сообщений: 160

RAII: внутри функции и можно ли в ней заменить new?

12.02.2017, 16:05. Показов 1391. Ответов 15
Метки нет (Все метки)

Author24 — интернет-сервис помощи студентам
По наводке Убежденный стал разбираться с RAII, но по мере чтения инфы по сабжу возникают вопросы. Например, как создавать RAII для нескольких функций, требующих закрытия\высвобождения ресурсов? Допустим, хочу проверить является ли пользователь, запускающий мое приложение, администратором, для этого вызываю OpenProcessToken для текущего процесса, тем самым после нам нужно закрыть полученный токен, далее считываю информацию о токене GetTokenInformation: здесь сперва нужно узнать размер буфера для TOKEN_GROUPS, поэтому придется использовать оператор new для выделения нужной памяти (или все же стоит использовать вектор?!); ну и в конце Sid полученный AllocateAndInitializeSid сравниваю с тем, что в группе TOKEN_GROUPS. Псевдокод:
C++
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
BOOLEAN IsUserAdmin {
BOOLEN isAdmin = FALSE;
 
// здесь RAII для OpenProcessToken (CloseToken)
// и AllocateAndInitializeSid (FreeSid)
// как RAII объявить?
 
if (!OpenProcessToken(GetCurrentProcess(), TOKEN_QUERY, &token)) {
   return err_code;
}
 
if (!GetTokenInformation(token, TokenGroups, NULL, 0, &buff_size &&
           ERROR_INSUFFICIENT_BUFFER != GetLastError()) {
   return err_code;
}
 
buff_size = new ...;
if (!GetTokenInformation(token, TokenGroups, token_groups, biff_size, &buff_size) {
   // высвободить память delete
   return err_code;
}
 
if (!AllocateAndInitializeSid(... &sid)) {
   return err_code;
}
 
// далее сравниваем Sid
for (int i = 0; i < token_groups->GroupsCount; i++) {
   if (EqualSid(sid, token_groups->Groups[i].Sid)) {
     isAdmin = TRUE;
     break;
   }
}
}
Можно ли обойтись без new? Или можно и для new создавать RAII?
0
Programming
Эксперт
39485 / 9562 / 3019
Регистрация: 12.04.2006
Сообщений: 41,671
Блог
12.02.2017, 16:05
Ответы с готовыми решениями:

Почему выделенная внутри функции память удаляется после возврата функции? Это можно исправить?
Вот пример функции, которая выделяет память под переменную, объявленную за её пределами: void Foo (wchar_t* test) { test =...

Можно ли сделать print внутри функции?
если да, то как? если нет, то как можно это обойти?

Можно ли внутри самого регулярного выражения использовать функции
Здравствуйте! Я бы хотела спросить, можно ли внутри самого регулярного выражения использовать функции заданные раньше? Например, функцию...

15
Ушел с форума
Эксперт С++
 Аватар для Убежденный
16478 / 7441 / 1187
Регистрация: 02.05.2013
Сообщений: 11,617
Записей в блоге: 1
14.02.2017, 15:04
Цитата Сообщение от jkadaba Посмотреть сообщение
Например, как создавать RAII для нескольких функций, требующих закрытия\высвобождения ресурсов?
Единого общепринятого подхода нет.
Я, например, предпочитаю иметь несколько маленьких классов-оберток:
auto_handle для HANDLE, auto_free для LocalFree и т.д.
Если логика работы с функцией не совсем простая, она вся заворачивается в
класс-обертку, а наружу выставляются простые и безопасные методы,
которые трудно использовать не по назначению. Если идет работа с буферами,
то всяким malloc/new/etc я предпочитаю сразу брать std::vector. И т.д.
0
176 / 2 / 1
Регистрация: 31.10.2016
Сообщений: 160
14.02.2017, 16:41  [ТС]
Убежденный, а можно конкретные примеры? То что в Вики лично мне этого мало, чтобы переварить концепцию RAII. Вот как, например, выделить память под некую структуру и если функция вернула length mismatch, как мне увеличить размер вектора?
Где почитать про автохэндл, автофри и так далее? Мне нужны корректные примеры, чтобы понять что к чему.
0
Ушел с форума
Эксперт С++
 Аватар для Убежденный
16478 / 7441 / 1187
Регистрация: 02.05.2013
Сообщений: 11,617
Записей в блоге: 1
14.02.2017, 18:07
Лучший ответ Сообщение было отмечено jkadaba как решение

Решение

Обычно я использую примерно следующий подход:
C++
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
bool IsUserAdmin()
{
    auto_handle Token;
    
    if (!OpenProcessToken(GetCurrentProcess(), TOKEN_QUERY, &Token)) {
        ThrowExc("OpenProcessToken failed, err = 0x%.8lx.", GetLastError());
    }
 
    DWORD ReqSize;
    
    if (GetTokenInformation(Token, TokenGroups, NULL, 0, &ReqSize)) {
        ThrowExc("GetTokenInformation #1 returns TRUE.");
    }
    
    DWORD const LastError = GetLastError();
    if (ERROR_INSUFFICIENT_BUFFER != LastError) {
        ThrowExc("GetTokenInformation #1 failed, err = 0x%.8lx.", LastError);
    }    
    
    DWORD const BuffSize = ReqSize;
    std::vector<byte> Buffer(BuffSize);
    
    if (!GetTokenInformation(Token, TokenGroups, &Buffer[0], BuffSize, &ReqSize)) {
        ThrowExc("GetTokenInformation #2 failed, err = 0x%.8lx.", GetLastError());
    }
    
    DWORD SidBuffSize = SECURITY_MAX_SID_SIZE;
    std::vector<byte> SidBuffer(SidBuffSize);
    
    if (!CreateWellKnownSid(WinBuiltinAdministratorsSid, NULL, &SidBuffer[0], &SidBuffSize)) {
        ThrowExc("CreateWellKnownSid failed, err = 0x%.8lx.", GetLastError());
    }
    
    TOKEN_GROUPS const * pInfo = reinterpret_cast<TOKEN_GROUPS *>(&Buffer[0]);
    DWORD const Count = pInfo->GroupCount;
    
    for (DWORD i = 0; i < Count; ++i)
    {
        if (pInfo->Groups[i].Attributes & (SE_GROUP_ENABLED | SE_GROUP_ENABLED_BY_DEFAULT))
        {
            if (EqualSid(pInfo->Groups[i].Sid, &SidBuffer[0]))
            {
                return true;
            }
        }
    }
    
    return false;
}
Как должно быть понятно из кода, все ресурсы, т.е. хэндл токена, а также
буферы для TOKEN_GROUPS и SID, при выходе из функции или по исключению
автоматически освобождаются. Это и есть RAII.

Класс auto_handle можно реализовать, например, так:
C++
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
class auto_handle
{
public:
    auto_handle(HANDLE h = NULL) :
        m_h(h)
    {
    }
 
    ~auto_handle()
    {
        if ( (m_h) && (INVALID_HANDLE_VALUE != m_h) )
        {
            CloseHandle(m_h);
        }
    }
 
    operator HANDLE ()
    {
        return m_h;
    }
 
    HANDLE * operator & ()
    {
        return (&m_h);
    }
 
private:
    auto_handle(auto_handle const &);
    auto_handle & operator = (auto_handle const &);
 
private:
    HANDLE m_h;
};
Это простой и надежный подход. Хотя кому-то может показаться, что здесь
все равно слишком много низкоуровневых деталей. В большом серьезном проекте я
предпочту разработать сначала готовые классы-обертки для работы с ресурсами, а
затем использовать их. Получится что-то вроде такого:
C++
1
2
3
4
5
6
7
8
9
bool IsUserAdmin()
{
    using namespace Security;
    
    safe_handle Token = openToken(tokenType::process, rights::tokenQuery);
    groups Groups = Token.getGroups();
    sid Sid = createSid(wellKnown::builtinAdmin);
    return (Groups.isPresent(Sid));
}
P.S.
А еще проще вызвать CheckTokenMembership и не заморачиваться
0
176 / 2 / 1
Регистрация: 31.10.2016
Сообщений: 160
14.02.2017, 20:55  [ТС]
Про CheckTokenMembership в курсе, здесь речь не столько о нем, сколько попытка на каком-нибудь примере понять суть RAII. С классом автохэндл разобрался, с вектором картина прояснилась, но вот чего никак не пойму зачем порождать исключение, если апишная функция завершилась неудачно. Не проще ли для этих целей использовать __try ... __leave? Ну и все же, можно как-то запилить в функцию RAII сразу для нескольких апишинных функций?
0
Ушел с форума
Эксперт С++
 Аватар для Убежденный
16478 / 7441 / 1187
Регистрация: 02.05.2013
Сообщений: 11,617
Записей в блоге: 1
14.02.2017, 21:37
Цитата Сообщение от jkadaba Посмотреть сообщение
зачем порождать исключение, если апишная функция завершилась неудачно.
Вопрос вкуса.
Для кода, перфоманс которого не является критичным, исключения банально удобнее (IMHO).
Потому что клиентскому коду не требуется писать 150 проверок кодов ошибок.

Цитата Сообщение от jkadaba Посмотреть сообщение
Не проще ли для этих целей использовать __try ... __leave?
__try/__leave - это нестандартно, и эта конструкция имеет нежелательные побочные эффекты
(например, переход в блок __finally при возникновения деления на ноль).

Цитата Сообщение от jkadaba Посмотреть сообщение
Ну и все же, можно как-то запилить в функцию RAII сразу для нескольких апишинных функций?
Ну сделай класс, в котором бы хранились результаты вызова нескольких функций.
И в деструкторе разом все ресурсы освобождай.
0
Покинул форум
3700 / 1483 / 355
Регистрация: 07.05.2015
Сообщений: 2,903
16.02.2017, 09:52
Лучший ответ Сообщение было отмечено jkadaba как решение

Решение

Попробую телепатировать, что подразумевалось под "запилом" RAII в функцию:
C++
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
BOOL IsUserAdmin(void) {
  struct Res {
    HANDLE token;
    PSID   sid;
    
    Res() : sid(NULL), token(NULL) {}
    ~Res() {
      if (sid) FreeSid(sid);
      if (token) CloseHandle(token);
    }
  } res;
  
  BOOL status = FALSE;
  /* прочие переменные */
  
  if (!OpenProcessToken(
    GetCurrentProcess(), TOKEN_QUERY, &res.token
  )) return status;
  
  /* прочие вызовы */
}
1
176 / 2 / 1
Регистрация: 31.10.2016
Сообщений: 160
16.02.2017, 12:31  [ТС]
Я это и имел в виду. А насколько корректен будет данный подход с точки высвобождения ресурсов? Убежденный, что скажешь? Можно так?
0
Ушел с форума
Эксперт С++
 Аватар для Убежденный
16478 / 7441 / 1187
Регистрация: 02.05.2013
Сообщений: 11,617
Записей в блоге: 1
16.02.2017, 16:07
Цитата Сообщение от jkadaba Посмотреть сообщение
А насколько корректен будет данный подход с точки высвобождения ресурсов?
А какие здесь проблемы? Я проблем не вижу.
На выходе из функции sid и т.п. будут очищены.
0
176 / 2 / 1
Регистрация: 31.10.2016
Сообщений: 160
16.02.2017, 19:32  [ТС]
Спасибо, ребят!
0
176 / 2 / 1
Регистрация: 31.10.2016
Сообщений: 160
20.09.2017, 12:25  [ТС]
Убежденный, а где можно посмотреть примеры классов оберток auto_handle, auto_free и так далее? Можешь поделиться наработками и опытом? Просто тема лично меня очень интересует.
0
Ушел с форума
Эксперт С++
 Аватар для Убежденный
16478 / 7441 / 1187
Регистрация: 02.05.2013
Сообщений: 11,617
Записей в блоге: 1
20.09.2017, 13:20
К сожалению, не могу привести код именно тех классов, которыми пользуюсь я
(т.к. они часть нашего проекта с закрытыми исходниками), а хотелось бы.
Но это все очень легко слепить самому из ниток, бумаги и пластилина.


Пример с auto_handle есть выше, по аналогии пишется все остальное.
Можно вообще сделать шаблонные классы с готовыми базовыми функциями,
останется только наследоваться от них или написать один раз typedef...

На самом деле RAII в виде scope-оберток для каких-то локально используемых ресурсов
типа HANDLE - это лишь первый маленький шаг к написанию надежного и безопасного кода.
Дальше надо все "сырые" буферы заменить на vector/array/etc, вместо C-строк использовать
std::string/QString/CString/etc, поменьше работать с "сырыми" указателями (предпочитая
им указатели "умные") и так далее.
1
232 / 135 / 19
Регистрация: 10.11.2015
Сообщений: 305
20.09.2017, 16:57
Лучший ответ Сообщение было отмечено jkadaba как решение

Решение

jkadaba, я делаю так:

C++
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
//
// Auto.hpp
//
 
#pragma once
 
#include <Windows.h>
 
namespace Auto {
 
template <typename T, T INVALID> 
class AutoCleanup {
 
protected:
 
    AutoCleanup(T Res) : m_Res(Res) {}
 
public:  
 
    bool IsValid() const
    {
        return INVALID != m_Res;
    }
 
    operator T() const 
    { 
        ASSERT(true == IsValid());
        return m_Res; 
    }
 
    T* operator&()
    {
        ASSERT(false == IsValid());
        return &m_Res;
    }
 
    T operator=(T Res)
    { 
        ASSERT(false == IsValid());
        m_Res = Res;
        return m_Res;  
    }
 
    void Release()
    {
        ASSERT(true == IsValid());
        m_Res = INVALID;
    }
 
protected:
 
    T m_Res;
 
private:
 
    AutoCleanup(const AutoCleanup<T, INVALID>&);
    AutoCleanup<T, INVALID>& operator=(const AutoCleanup<T, INVALID>&);
};
 
//
// Simple classes
//
 
#define AUTO_CLEANUP_CLASS(FUNC, T, INVALID, NAME)          \
                                                            \
    struct NAME : public Auto::AutoCleanup<T, INVALID> {    \
                                                            \
        NAME(T Res = INVALID) : AutoCleanup(Res) {}         \
                                                            \
        ~NAME()                                             \
        {                                                   \
            if (IsValid()) {                                \
                FUNC(m_Res);                                \
            }                                               \
        }                                                   \
                                                            \
        T operator=(T Res)                                  \
        {                                                   \
            return AutoCleanup::operator=(Res);             \
        }                                                   \
    };
 
AUTO_CLEANUP_CLASS(::CloseHandle, HANDLE, NULL, AutoCloseHandle)
AUTO_CLEANUP_CLASS(::CloseHandle, HANDLE, INVALID_HANDLE_VALUE, AutoCloseFile)
AUTO_CLEANUP_CLASS(::CloseServiceHandle, SC_HANDLE, NULL, AutoCloseServiceHandle)
AUTO_CLEANUP_CLASS(::FreeLibrary, HMODULE, NULL, AutoFreeLibrary)
AUTO_CLEANUP_CLASS(::LocalFree, HLOCAL, NULL, AutoLocalFree)
AUTO_CLEANUP_CLASS(::UnmapViewOfFile, PVOID, NULL, AutoUnmapViewOfFile)
AUTO_CLEANUP_CLASS(::CloseDesktop, HDESK, NULL, AutoCloseDesktop)
AUTO_CLEANUP_CLASS(::CloseWindowStation, HWINSTA, NULL, AutoCloseWindowStation)
AUTO_CLEANUP_CLASS(::RegCloseKey, HKEY, NULL, AutoRegCloseKey)
 
//
// AutoVirtualFree
//
 
struct AutoVirtualFree : public AutoCleanup<PVOID, NULL> {
 
    AutoVirtualFree(PVOID Address = NULL, SIZE_T Size = 0, DWORD FreeType = MEM_RELEASE) 
    : 
    AutoCleanup(Address), m_Size(Size), m_FreeType(FreeType) {}
 
    ~AutoVirtualFree()
    {
        if (true == IsValid()) {
            ::VirtualFree(m_Res, m_Size, m_FreeType);
        }
    }
 
    HANDLE operator=(HANDLE Res)
    { 
        return AutoCleanup::operator=(Res);  
    }
 
private:
 
    const SIZE_T m_Size;
    const DWORD m_FreeType;
};
 
}; // namespace Auto
В Auto.hpp держу часто используемые классы. По мере надобности объявляю новые:

C++
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
//
// ScreenCreator.cpp
//
 
    ...
 
#include "Auto.hpp"
 
#pragma comment(lib, "gdiplus.lib" )
 
using namespace Gdiplus;
using namespace Gdiplus::DllExports;
 
namespace Auto {
 
    AUTO_CLEANUP_CLASS(DeleteDC, HDC, NULL, AutoDeleteDC)
    AUTO_CLEANUP_CLASS(DeleteObject, HBITMAP, NULL, AutoDeleteObject)
    AUTO_CLEANUP_CLASS(GdipDisposeImage, GpBitmap*, NULL, AutoGdipDisposeImage)
    
    //
    // AutoReleaseDC
    //
    
    struct AutoReleaseDC : public AutoCleanup<HDC, NULL> {
 
        AutoReleaseDC(HDC hDC = NULL, HWND hWnd = NULL) 
        : 
        AutoCleanup(hDC), m_hWnd(hWnd) {}
 
        ~AutoReleaseDC()
        {
            if (true == IsValid()) {
                ::ReleaseDC(m_hWnd, m_Res);
            }
        }
 
        HDC operator=(HDC Res)
        { 
            return AutoCleanup::operator=(Res);  
        }
 
    private:
 
        const HWND m_hWnd;
    };
 
}; // namespace Auto
 
    ...
2
Ушел с форума
Эксперт С++
 Аватар для Убежденный
16478 / 7441 / 1187
Регистрация: 02.05.2013
Сообщений: 11,617
Записей в блоге: 1
20.09.2017, 17:32
jupman, симпатичненько
У меня, кстати, базовые классы тоже лежат в Auto.hpp (совпадение?).
Все хорошо, а вот операторы взятия адреса и неявные приведения к типу T я бы убрал, IMHO.
От них больше проблем, чем пользы.
1
176 / 2 / 1
Регистрация: 31.10.2016
Сообщений: 160
27.09.2017, 11:46  [ТС]
jupman, хотел бы отметить твой ответ, но почему-то кнопка с плюсом не срабатывает.
Убежденный, а как быть с аргументами командной строки. В смысле:
C++
1
2
3
int main(int argc, char *argv[]) {
  ...
}
Приводить аргументы к типу string или что? Вообще, как можно избавиться от char * и подобных ему PTCHAR и т.д.?
0
Покинул форум
3700 / 1483 / 355
Регистрация: 07.05.2015
Сообщений: 2,903
27.09.2017, 19:07
jkadaba, понимаю, что только изучаете, но что ж Вы так бестолковите? Убежденный уже не раз Вам говорил про векторы и строки. Возьмите, да запихните аргументы в вектор:
C++
1
2
3
4
int main(int argc, char * argv[]) {
   std:vector<std::string> args(argv, argv + argc);
   ...
}
И обращайтесь к аргументам как к строкам:
C++
1
2
3
...
   std::cout <<  args[0] << std::endl;
...
0
Надоела реклама? Зарегистрируйтесь и она исчезнет полностью.
inter-admin
Эксперт
29715 / 6470 / 2152
Регистрация: 06.03.2009
Сообщений: 28,500
Блог
27.09.2017, 19:07
Помогаю со студенческими работами здесь

Можно ли внутри клиентской функции изменять серверную переменную?
Привет! Можно ли внутри клиентской функции изменять серверную переменную. Серверная переменная schet объявлена следующим образом: ...

Передача параметров функциям. Можно ли изменить этот параметр внутри функции
Доброго времени суток, господа знатоки.В универе препод задал сделать проверку входных данных на ошибку.Я полазил по форумам и нашел вот...

Можно ли внутри функции f_1 создать массив размера, заданного аргументом n_1?
Ситуация простая. Язык С++. Есть функция, пусть будет f_1, которая получает аргумент n_1, натурального типа. Вопрос такой: внутри функции...

Каким образом можно во внешнем запросе выбрать все переменные, которые находятся внутри функции?
Добрый день. У меня есть пакет с функцией внутри которой производятся различные действия со многими переменными. Каким образом...

Дан указатель: double **p = 0; Выполните следующие задания (решения можно оформлять внутри функции main):
Дан указатель: double **p = 0; Выполните следующие задания (решения можно оформлять внутри функции main): * создайте конструкцию,...


Искать еще темы с ответами

Или воспользуйтесь поиском по форуму:
16
Ответ Создать тему
Опции темы

Новые блоги и статьи
Чем асинхронная логика (схемотехника) лучше тактируемой, как я думаю, что помимо энергоэффективности - ещё и безопасность.
Hrethgir 14.05.2025
Помимо огромного плюса в энергоэффективности, асинхронная логика - тотальный контроль над каждым совершённым тактом, а значит - безусловная безопасность, где безконтрольно не совершится ни одного. . .
Многопоточные приложения на C++
bytestream 14.05.2025
C++ всегда был языком, тесно работающим с железом, и потому особеннно эффективным для многопоточного программирования. Стандарт C++11 произвёл революцию, добавив в язык нативную поддержку потоков,. . .
Stack, Queue и Hashtable в C#
UnmanagedCoder 14.05.2025
Каждый опытный разработчик наверняка сталкивался с ситуацией, когда невинный на первый взгляд List<T> превращался в узкое горлышко всего приложения. Причина проста: универсальность – это прекрасно,. . .
Как использовать OAuth2 со Spring Security в Java
Javaican 14.05.2025
Протокол OAuth2 часто путают с механизмами аутентификации, хотя по сути это протокол авторизации. Представьте, что вместо передачи ключей от всего дома вашему другу, который пришёл полить цветы, вы. . .
Анализ текста на Python с NLTK и Spacy
AI_Generated 14.05.2025
NLTK, старожил в мире обработки естественного языка на Python, содержит богатейшую коллекцию алгоритмов и готовых моделей. Эта библиотека отлично подходит для образовательных целей и. . .
Реализация DI в PHP
Jason-Webb 13.05.2025
Когда я начинал писать свой первый крупный PHP-проект, моя архитектура напоминала запутаный клубок спагетти. Классы создавали другие классы внутри себя, зависимости жостко прописывались в коде, а о. . .
Обработка изображений в реальном времени на C# с OpenCV
stackOverflow 13.05.2025
Объединение библиотеки компьютерного зрения OpenCV с современным языком программирования C# создаёт симбиоз, который открывает доступ к впечатляющему набору возможностей. Ключевое преимущество этого. . .
POCO, ACE, Loki и другие продвинутые C++ библиотеки
NullReferenced 13.05.2025
В C++ разработки существует такое обилие библиотек, что порой кажется, будто ты заблудился в дремучем лесу. И среди этого многообразия POCO (Portable Components) – как маяк для тех, кто ищет. . .
Паттерны проектирования GoF на C#
UnmanagedCoder 13.05.2025
Вы наверняка сталкивались с ситуациями, когда код разрастается до неприличных размеров, а его поддержка становится настоящим испытанием. Именно в такие моменты на помощь приходят паттерны Gang of. . .
Создаем CLI приложение на Python с Prompt Toolkit
py-thonny 13.05.2025
Современные командные интерфейсы давно перестали быть черно-белыми текстовыми программами, которые многие помнят по старым операционным системам. CLI сегодня – это мощные, интуитивные и даже. . .
КиберФорум - форум программистов, компьютерный форум, программирование
Powered by vBulletin
Copyright ©2000 - 2025, CyberForum.ru