Re[11]: Вызов виртуального метода для класса
От: Chez Россия  
Дата: 20.10.04 07:39
Оценка:
Здравствуйте, erithion aka tyomik, Вы писали:

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

А можно пример, когда такое возможно?
Chez, ICQ# 161095094
Re[4]: Вызов виртуального метода для класса
От: Аноним  
Дата: 20.10.04 07:53
Оценка: :))
Здравствуйте, jazzer, Вы писали:

J>Потому что Стандарт С++ не оговаривает машинный код методов, более того, в Стандарте нет понятия виртуальной таблицы. (попробуй поискать в тексте Стандарта vtbl или vmt на досуге)

J>Ты оперируешь терминами конкретной реализации, выходящими за пределы Стандарта, а задаешь вопрос о решении в рамках Стандарта.
J>Решения в рамках Стандарта не существует.
J>Существуют платформенно-зависимые решения, начиная с "определенного" неопределенного поведения и кончая asm.

Аминь.
Re[12]: Вызов виртуального метода для класса
От: erithion aka tyomik  
Дата: 21.10.04 18:17
Оценка:
Здравствуйте, Chez, Вы писали:

C>Здравствуйте, erithion aka tyomik, Вы писали:


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

C>А можно пример, когда такое возможно?
Пожалуйста:
class a
{
public:
    a()
    {
        printf("a::constructor");
    }
    virtual void fn()
    {
        printf("a::fn");
    }
};
class c: public virtual a
{
public:
    c()
    {
        printf("c::constructor");
    }
    void fn()
    {
        printf("c::fn");
    }
};

int _tmain(int argc, _TCHAR* argv[])
{
    c* i = new c();
    i->fn();
    delete i;
    return 0;
}

Вот более или менее понятный разбор того, что происходит в бин. коде:

Кострукторы
.text:00401110     a_constructor   proc near               ; CODE XREF: c_constructor+1Cp
...........................
.text:00401133     a_constructor   endp
.text:004010A0     c_constructor   proc near               ; CODE XREF: _main+3Ap
.text:004010A0
.text:004010A0     l_mem           = dword ptr -4
.text:004010A0     arg_0           = dword ptr  8
.text:004010A0
.text:004010A0 000                 push    ebp
.text:004010A1 004                 mov     ebp, esp
.text:004010A3 004                 push    ecx
.text:004010A4 008                 mov     [ebp+l_mem], ecx
.text:004010A7 008                 cmp     [ebp+arg_0], 0
.text:004010AB 008                 jz      short if_zero
.text:004010AD 008                 mov     eax, [ebp+l_mem]
.text:004010B0 008                 mov     dword ptr [eax], offset init ; structure init contains offsets.
.text:004010B0                                             ; Let's imagine them as indicies;
.text:004010B0                                             ; int init[] = {0, 2};
.text:004010B0                                             ; this[0] = &init;
.text:004010B6 008                 mov     ecx, [ebp+l_mem]
.text:004010B9 008                 add     ecx, 8
.text:004010BC 008                 call    a_constructor   ; this[2] = a_vtbl;
.text:004010C1
.text:004010C1     if_zero:                                ; CODE XREF: c_constructor+Bj
.text:004010C1 008                 mov     ecx, [ebp+l_mem]
.text:004010C4 008                 mov     edx, [ecx]
.text:004010C6 008                 mov     eax, [edx+4]
.text:004010C9 008                 mov     ecx, [ebp+l_mem]
.text:004010CC 008                 mov     dword ptr [ecx+eax], offset c_vtbl ; this[init[1]] = c_vtbl;
.text:004010D3 008                 mov     edx, [ebp+l_mem]
.text:004010D6 008                 mov     eax, [edx]
.text:004010D8 008                 mov     ecx, [eax+4]
.text:004010DB 008                 sub     ecx, 8
.text:004010DE 008                 mov     edx, [ebp+l_mem]
.text:004010E1 008                 mov     eax, [edx]
.text:004010E3 008                 mov     edx, [eax+4]
.text:004010E6 008                 mov     eax, [ebp+l_mem]
.text:004010E9 008                 mov     [eax+edx-4], ecx ; this[init[1] - 1] = init[1] - 2;
.text:004010ED 008                 push    offset aCConstructor ; "c::constructor"
.text:004010F2 00C                 call    _printf
.text:004010F7 00C                 add     esp, 4
.text:004010FA 008                 mov     eax, [ebp+l_mem]
.text:004010FD 008                 mov     esp, ebp
.text:004010FF 004                 pop     ebp
.text:00401100 000                 retn    4
.text:00401100     c_constructor   endp

main
.text:00401000     _main           proc near               ; CODE XREF: start+16Ep
.text:00401000
.text:00401000     i               = dword ptr -20h
.text:00401000     var_1C          = dword ptr -1Ch
.text:00401000     l_mem           = dword ptr -18h
.text:00401000     var_14          = dword ptr -14h
.text:00401000     var_10          = dword ptr -10h
.text:00401000     var_C           = dword ptr -0Ch
.text:00401000     var_4           = dword ptr -4
.text:00401000
.text:00401000 000                 push    ebp
.text:00401001 004                 mov     ebp, esp
..............................
.text:0040101D 028                 call    operator new(uint)
.text:00401022 028                 add     esp, 4
.text:00401025 024                 mov     [ebp+l_mem], eax
.text:00401028 024                 mov     [ebp+var_4], 0
.text:0040102F 024                 cmp     [ebp+l_mem], 0
.text:00401033 024                 jz      short if_fail   ; i = 0;
.text:00401035 024                 push    1
.text:00401037 028                 mov     ecx, [ebp+l_mem]
.text:0040103A 028                 call    c_constructor
.text:0040103F 024                 mov     [ebp+i], eax    ; i = new c();
.text:00401042 024                 jmp     short proceed
.text:00401044     ; ---------------------------------------------------------------------------
.text:00401044
.text:00401044     if_fail:                                ; CODE XREF: _main+33j
.text:00401044 024                 mov     [ebp+i], 0      ; i = 0;
.text:0040104B
.text:0040104B     proceed:                                ; CODE XREF: _main+42j
.text:0040104B 024                 mov     eax, [ebp+i]
.text:0040104E 024                 mov     [ebp+var_14], eax
.text:00401051 024                 mov     [ebp+var_4], 0FFFFFFFFh
.text:00401058 024                 mov     ecx, [ebp+var_14]
.text:0040105B 024                 mov     [ebp+var_10], ecx
.text:0040105E 024                 mov     edx, [ebp+var_10]
.text:00401061 024                 mov     eax, [edx]
.text:00401063 024                 mov     ecx, [eax+4]
.text:00401066 024                 mov     edx, [ebp+var_10]
.text:00401069 024                 mov     eax, [edx]
.text:0040106B 024                 mov     edx, [ebp+var_10]
.text:0040106E 024                 add     edx, [eax+4]
.text:00401071 024                 mov     eax, [ebp+var_10]
.text:00401074 024                 mov     eax, [eax+ecx]
.text:00401077 024                 mov     ecx, edx        
.text:00401079 024                 call    dword ptr [eax] ; call dword ptr [this[init[1]]]; //  i->fn();
................................................................
.text:00401099 024                 mov     esp, ebp
.text:0040109B 004                 pop     ebp
.text:0040109C 000                 retn
.text:0040109C     _main           endp

Невиртуальная ф-я класса с
.text:00401160     c_fn            proc near               ; DATA XREF: .rdata:c_vtblo
.text:00401160
.text:00401160     var_4           = dword ptr -4
.text:00401160
.text:00401160 000                 sub     ecx, [ecx-4]
.text:00401163 000                 jmp     loc_401170
.text:00401163     ; ---------------------------------------------------------------------------
.text:00401168 000                 align 10h
.text:00401170
.text:00401170     loc_401170:                             ; CODE XREF: c_fn+3j
.text:00401170 000                 push    ebp
.text:00401171 004                 mov     ebp, esp
.text:00401173 004                 push    ecx
.text:00401174 008                 mov     [ebp+var_4], ecx
.text:00401177 008                 push    offset aCFn     ; "c::fn"
.text:0040117C 00C                 call    _printf
.text:00401181 00C                 add     esp, 4
.text:00401184 008                 mov     esp, ebp
.text:00401186 000                 pop     ebp
.text:00401187 -04                 retn
.text:00401187     c_fn            endp ; sp =  4

Виртуальные таблицы и таблица индексов
.rdata:0040811C     c_vtbl          dd offset c_fn          ; DATA XREF: c_constructor+2Co
.rdata:00408120     init            dd 0                    ; DATA XREF: c_constructor+10o
.rdata:00408124                     dd 8
.rdata:00408138     a_vtbl          dd offset a_fn          ; DATA XREF: a_constructor+Ao

Вкратце, происходит следующее:
Внутри конструктора класса с видим, что он в самое начало вирт. таблицы записывает некую таблицу индексов, которой потом пользуется для инициализации и позднее поиска виртуальных таблиц и ф-й в них. Мои комментарии справа подразумевают, что в таблице смещения не в байтах, а в DWORD'ах, т.е. индексы.
Далее происходит вызов конструктора класса а, который записывает во 2-й элемент таблицы адрес вирт. таблицы класса а. После этого происходит замещение этого адреса адресом вирт. таблицы класса с, которая вмещает в себя невиртуальную ф-ю в классе с. После выполняется все остальное тело конструктора.
В main можно увидеть, как вызов i->fn(); происходит косвенно через регистр еах.
P.S.: Спасибо за вопрос. Это заинтересовало меня, поскольку в VS 2003 .NET сишный компилер генерит несколько иной код. В частности меня заинтересовал единственный параметр в конструкторе класса с равный 1. В конструкторе же можно видеть, что ежели этот параметр равен нулю, то вызова конструктора базового класса не произойдет. Пока я не знаю к какой опере это относится и как можно добится того, чтоб конструктор предка не вызывался вовсе. Кроме того, при обычном наследовании(невиртуальном) у конструктора отсутствует этот параметр вообще. Так что это повод провести более полный анализ.
Thx!!!
... locked in silent monolog ...
Re[13]: Вызов виртуального метода для класса
От: elcste  
Дата: 22.10.04 02:22
Оценка: +1
Здравствуйте, erithion aka tyomik, Вы писали:

EAT>>>Забыл упомянуть, что вызов невиртуальных методов всегда происходит непосредственно, по адресу, за исключением сложных случаев наследования, когда невиртуальный метод таки попадает в виртуальную таблицу и вызывается косвенно.

EAT>class a
EAT>{
EAT>public:
EAT>    a()
EAT>    {
EAT>        printf("a::constructor");
EAT>    }
EAT>    virtual void fn()
EAT>    {
EAT>        printf("a::fn");
EAT>    }
EAT>};
EAT>class c: public virtual a
EAT>{
EAT>public:
EAT>    c()
EAT>    {
EAT>        printf("c::constructor");
EAT>    }
EAT>    void fn()
EAT>    {
EAT>        printf("c::fn");
EAT>    }
EAT>};

c::fn — виртуальная функция.

EAT>P.S.: Спасибо за вопрос. Это заинтересовало меня, поскольку в VS 2003 .NET сишный компилер генерит несколько иной код. В частности меня заинтересовал единственный параметр в конструкторе класса с равный 1. В конструкторе же можно видеть, что ежели этот параметр равен нулю, то вызова конструктора базового класса не произойдет. Пока я не знаю к какой опере это относится и как можно добится того, чтоб конструктор предка не вызывался вовсе. Кроме того, при обычном наследовании(невиртуальном) у конструктора отсутствует этот параметр вообще. Так что это повод провести более полный анализ.


Виртуальные базовые классы инициализируются только из конструктора most derived class.


P.S. Учите язык.
Re[13]: Вызов виртуального метода для класса
От: Chez Россия  
Дата: 22.10.04 09:10
Оценка:
EAT>P.S.: Спасибо за вопрос.
Всегда пожалуйста
Chez, ICQ# 161095094
Re[14]: Вызов виртуального метода для класса
От: erithion aka tyomik  
Дата: 22.10.04 12:36
Оценка:
Здравствуйте, elcste, Вы писали:

E>Здравствуйте, erithion aka tyomik, Вы писали:

E>c::fn — виртуальная функция.

Еще раз повторюсь, меня не интересуют стандарты, несмотря на то, что я тоже читал Страуструпа и другую не менее интересную литературу и тоже прекрасно помню, что всеми ими такой метод класса обзывается виртуальным.
С точки же зрения исследователя, я знаю другое, а именно, что это чистый метод класса в реализации МС С++ компилятора. Его виртуальность возникает вследствие перекрытия имен, которая разрешается в пользу виртуальности метода наследника. И виртуальность его косвенная, поскольку реализуется через переходник. На что собсно код и указывает. В виртуальной таблице класса с не адрес самой процедуры, а адрес так называемого переходника на процедуру(метод класса c::fn) с коррекцией указателя перед переходом. Обрати внимание еще раз, мож теперь понятнее буит:

.text:00401160     c_thunk_fn      proc near               ; DATA XREF: .rdata:c_vtblo
.text:00401160
.text:00401160     var_4           = dword ptr -4
.text:00401160
.text:00401160 000                 sub     ecx, [ecx-4]
.text:00401163 000                 jmp     c_fn
.text:00401163     c_thunk_fn      endp
.text:00401170     -----------------------------------------------------------------------
.text:00401170     c_fn            proc near               ; CODE XREF: c_thunk_fn+3j
.text:00401170
.text:00401170     var_4           = dword ptr -4
.text:00401170
.text:00401170 000                 push    ebp
.text:00401171 004                 mov     ebp, esp
.text:00401173 004                 push    ecx
.text:00401174 008                 mov     [ebp+var_4], ecx
.text:00401177 008                 push    offset aCFn     ; "c::fn"
.text:0040117C 00C                 call    _printf
.text:00401181 00C                 add     esp, 4
.text:00401184 008                 mov     esp, ebp
.text:00401186 004                 pop     ebp
.text:00401187 000                 retn
.text:00401187     c_fn            endp

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

E>Виртуальные базовые классы инициализируются только из конструктора most derived class.

Я сказал про параметр, вследствие равенства нулю которого вызова конструктора базового класса вообще не происходит. Как бы я не наследовался, вызов все равно будет, т.е. если присутствует один параметр в конструктор, то пока что у меня он всегда был равен 1. Равенства же его 0 я пока не смог добиться. И пока не упомню ситуаций, когда такое возможно. Разве что для нужд компилятора в специфических ситуациях, когда тока так можно выйти из положения.


E>P.S. Учите язык.

Спасибо! Торжественно обещаю
А какой? Алгол?
Я слышал множество пунктов стандарта и предложений по реализации взято именно оттуда.
... locked in silent monolog ...
Re[15]: Вызов виртуального метода для класса
От: elcste  
Дата: 23.10.04 03:58
Оценка: :)
Здравствуйте, erithion aka tyomik, Вы писали:

E>>c::fn — виртуальная функция.


EAT>Еще раз повторюсь, меня не интересуют стандарты, несмотря на то, что я тоже читал Страуструпа и другую не менее интересную литературу и тоже прекрасно помню, что всеми ими такой метод класса обзывается виртуальным.


То есть эту функцию невиртуальной называете только Вы. Или еще кто-то?

EAT>С точки же зрения исследователя, я знаю другое, а именно, что это чистый метод класса в реализации МС С++ компилятора.


Вот только меня, в свою очередь, совершенно не интересует "реализация МС С++ компилятора". В данный момент я работаю с процессором Blackfin (ADSP-BF561), и мне интересен код, который генерирует VisualDSP++ от Analog Devices. Не хотите исследовать его для разнообразия?

    .file ".\test.cpp";

    .section program;

.epctext:

    .align 2;
_fn__1aFv:
// ".\test.cpp" line 12 col 18
        link  12;
// ".\test.cpp" line 14 col 9
//         -- 3 bubbles --
        R0.L = .epcrodata+16;
        R0.H = .epcrodata+16;
        CALL.X _printf;
        P0=[FP+ 4];
        unlink;
//         -- 3 bubbles --
        JUMP (P0);

._fn__1aFv.end:
    .type _fn__1aFv,STT_FUNC;

    .align 2;
_main:
// ".\test.cpp" line 31 col 5
        [--SP]=RETS;
        [--SP]=FP;
        FP = SP ;
        [--SP]=(P5:4);
//         -- 3 bubbles --
        SP +=  -12;
        CALL.X ___mark_dtor;
// ".\test.cpp" line 33 col 8
        R0 =   12;
        CALL.X ___nw__FUl;
        P5 = R0 ;
        CC =  R0 ==  0;
        P4 =   0;
        IF CC JUMP  ._P2L2 ;
// ".\test.cpp" line 10 col 9
        R0.L = .epcrodata;
        R0.H = .epcrodata;
// ".\test.cpp" line 33 col 8
        P4 = P5 ;
        R1.L = ___vtbl__1a;
        R1.H = ___vtbl__1a;
        P4 +=  8;
        [P5+ 4] = P4;
        [P5+ 8] = R1;
// ".\test.cpp" line 10 col 9
        CALL.X _printf;
        R0.L = ___vtbl__1c;
        R0.H = ___vtbl__1c;
        P0.L = .epcdata;
        P0.H = .epcdata;
        [P5+ 0] = R0;
        P0=[P0+ 4];
// ".\test.cpp" line 23 col 9
        R0.L = .epcrodata+24;
        R0.H = .epcrodata+24;
// ".\test.cpp" line 10 col 9
//         -- 2 bubbles --
        [P4+ 0] = P0;
// ".\test.cpp" line 23 col 9
        CALL.X _printf;
        P4 = P5 ;

._P2L2:
// ".\test.cpp" line 34 col 5
        P1=[P4+ 0];
//         -- 4 bubbles --
        R0=W[P1+ 8] (X);
        P0 = R0 ;
        P1=[P1+ 12];
//         -- 3 bubbles --
        P0 = P4 + P0;
        R0 = P0 ;
        CALL (P1);
// ".\test.cpp" line 35 col 5
        R0 = P4 ;
        CALL.X ___dl__FPv;
        SP +=  12;
        (P5:4)=[SP++];
// ".\test.cpp" line 36 col 5
//         -- 3 bubbles --
        R0 =   0;
        P2=[SP+ 4];
        FP=[SP+ 0];
        SP +=  8;
//         -- 2 bubbles --
        JUMP (P2);

._main.end:
    .global _main;
    .type _main,STT_FUNC;

    .align 2;
_fn__1cFv:
// ".\test.cpp" line 25 col 10
        link  12;
// ".\test.cpp" line 27 col 9
//         -- 3 bubbles --
        R0.L = .epcrodata+40;
        R0.H = .epcrodata+40;
        CALL.X _printf;
        P0=[FP+ 4];
        unlink;
//         -- 3 bubbles --
        JUMP (P0);

._fn__1cFv.end:
    .type _fn__1cFv,STT_FUNC;

.epctext.end:

    .extern _printf;
    .type _printf,STT_FUNC;
    .extern ___mark_dtor;
    .type ___mark_dtor,STT_FUNC;
    .extern ___nw__FUl;
    .type ___nw__FUl,STT_FUNC;
    .extern ___dl__FPv;
    .type ___dl__FPv,STT_FUNC;

    .section data1;

    .align 8;
.epcdata:
    .type .epcdata,STT_OBJECT;
.epc.cplus.compiled:
    .type .epc.cplus.compiled,STT_OBJECT;
    .byte = 0x00,0x00,0x00,0x00;
    .byte4 = ___vtbl__1a__1c;
.epcdata.end:

    .section constdata;

    .align 8;
.epcrodata:
    .type .epcrodata,STT_OBJECT;
    .byte = 0x61,0x3A,0x3A,0x63,0x6F,0x6E,0x73,0x74,0x72,0x75,0x63,0x74;
    .byte = 0x6F,0x72,0x00,0x00,0x61,0x3A,0x3A,0x66,0x6E,0x00,0x00,0x00;
    .byte = 0x63,0x3A,0x3A,0x63,0x6F,0x6E,0x73,0x74,0x72,0x75,0x63,0x74;
    .byte = 0x6F,0x72,0x00,0x00,0x63,0x3A,0x3A,0x66,0x6E,0x00;
.epcrodata.end:
    .align 4;
___vtbl__1a:
    .type ___vtbl__1a,STT_OBJECT;
    .byte = 0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00;
    .byte4 = _fn__1aFv;
.___vtbl__1a.end:
    .align 4;
___vtbl__1c:
    .type ___vtbl__1c,STT_OBJECT;
    .byte = 0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00;
    .byte4 = _fn__1cFv;
.___vtbl__1c.end:
    .align 4;
___vtbl__1a__1c:
    .type ___vtbl__1a__1c,STT_OBJECT;
    .byte = 0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0xF8,0xFF,0x00,0x00;
    .byte4 = _fn__1cFv;
.___vtbl__1a__1c.end:

    .section data1;

Обратите внимание на то, что вызов виртуальной функции c::fn происходит косвенно, через таблицу виртуальных функций, несмотря на совпадение в данном случае статического и динамического типов указуемого i объекта.

EAT>Его виртуальность возникает вследствие перекрытия имен, которая разрешается в пользу виртуальности метода наследника.


Bravo! You made my day!

E>>Виртуальные базовые классы инициализируются только из конструктора most derived class.

EAT>Я сказал про параметр, вследствие равенства нулю которого вызова конструктора базового класса вообще не происходит.

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

#include <iomanip>
#include <iostream>

struct foo
{
    bool qux;
    foo() : qux(true) {}
    foo(bool qux) : qux(qux) {}
};

struct bar : virtual foo
{
    bar() : foo(false) {}
};

struct baz : bar {};

int main()
{
    if(baz().qux)
        std::cout << "Surprise!" << std::endl;
}

E>>P.S. Учите язык.
EAT>Спасибо! Торжественно обещаю
EAT>А какой? Алгол?

Нет, русский.
Re[16]: Вызов виртуального метода для класса
От: erithion aka tyomik  
Дата: 24.10.04 19:39
Оценка:
Здравствуйте, elcste, Вы писали:

E>То есть эту функцию невиртуальной называете только Вы. Или еще кто-то?

Я б сказал, что для меня(и не только) она таковой не является именно потому, что стандартный подход реверсирования в этом случае несколько изменяется. Именно изза наличия переходника.

E>Вот только меня, в свою очередь, совершенно не интересует "реализация МС С++ компилятора". В данный момент я работаю с процессором Blackfin (ADSP-BF561), и мне интересен код, который генерирует VisualDSP++ от Analog Devices. Не хотите исследовать его для разнообразия?

Нет, спасибо, пока не хочу.Поскольку меня ждет не менее интересная работа с кодом ARM-процессора и тонкостями его построения компилятором.
Но раз уж речь зашла об этом, то компилер С++ для ARM'а вообще не признает виртуальное наследование. А вызов виртуальных методов производит непосредственно или же с jump'ом на код коррекции адреса перехода.
Так что не думаю что стоило здесь приводить подобные листинги, поскольку разговор шел о МС С++ в частности, и в целом для х86, если не ошибаюсь.

EAT>>Его виртуальность возникает вследствие перекрытия имен, которая разрешается в пользу виртуальности метода наследника.


E>Bravo! You made my day!

Я рад что тебе понравилось. Зови в следующий раз, посмешу

E>>>Виртуальные базовые классы инициализируются только из конструктора most derived class.

EAT>>Я сказал про параметр, вследствие равенства нулю которого вызова конструктора базового класса вообще не происходит.

E>Прочитав учебник, Вы осознаете смысл сказанного выше. Если же Вы принципиально не читаете ничего, кроме ассемблерных листингов, помедитируйте над результатом трансляции следующего примера.


За это спасибо. Это как раз то, что мне было нужно. Ответ прозвучал и раньше, но боюсь я его не сразу понял.
Так что иду к стандартам по виртуальному наследованию и не только
... locked in silent monolog ...
Подождите ...
Wait...
Пока на собственное сообщение не было ответов, его можно удалить.