Подтвердить что ты не робот

Что-то не так с использованием объекта в его собственной конструкции?

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

struct command
{
    command()
    {
    }

    command(const std::function<void()>& action)
        : action(action)
    {
    }

    int n;
    std::function<void()> action;
};

void test_action(command* this_ptr)
{
    this_ptr->n = 5;
}


int main()
{
    command com(std::bind(test_action, &com));
    com.action();
    std::cout << com.n;
}

Мой вопрос: безопасно ли делать command com(std::bind(test_action, &com));? Или это поведение undefined?

4b9b3361

Ответ 1

Во-первых: что такое объект?

[intro.object]\1

[...] Объект - это область хранения [...]

Хранилище выделяется до истечения времени жизни объекта:

[basic.life]

До начала жизни объекта, но после хранения который будет занимать объект, было выделено [..] любым указателем, который относится к месту хранения, где объект будет или находился могут использоваться, но только ограниченным образом. По строительству или разрушению объекта см. 12.7 [строительство и уничтожение]. В противном случае, такой указатель относится к выделенному хранилищу (3.7.4.2) и использует указатель, как если бы указатель имел тип void *, хорошо определен.

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

Это имеет смысл, поскольку в классическом компиляторе AST-моды, если вы посмотрите стандартную иерархию деклараторов, в простой игрушечный код, например

class command {
public:
  command(int) {
  }
};

int funct(command*) {
    return 2;
}

int main() {
    command com(funct(&com));
}

строка

command com(funct(&com));

интерпретируется следующим образом:

[dcl.decl]

simple-declaration:
    attribute-specifier-seqopt decl-specifier-seqopt init-declarator-listopt;
       ...
         initializer:
            brace-or-equal-initializer
                ( expression-list ) // The declaration statement is already specified

И, наконец, для вашего кода это то, как gcc компилирует эту строку (-O0)

command com(std::bind(test_action, &com));

->

movq    %rax, -104(%rbp)
leaq    -104(%rbp), %rdx
leaq    -96(%rbp), %rcx
movl    test_action(command*), %esi
movq    %rcx, %rdi
movq    %rax, -136(%rbp)        # 8-byte Spill
movq    %rcx, -144(%rbp)        # 8-byte Spill
callq   _ZSt4bindIRFvP7commandEJS1_EENSt12_Bind_helperIT_JDpT0_EE4typeEOS5_DpOS6_
leaq    -80(%rbp), %rax
movq    %rax, %rdi
movq    -144(%rbp), %rsi        # 8-byte Reload
movq    %rax, -152(%rbp)        # 8-byte Spill
callq   _ZNSt8functionIFvvEEC1ISt5_BindIFPFvP7commandES5_EEEET_NSt9enable_ifIXntsr11is_integralISA_EE5valueENS1_8_UselessEE4typeE
movq    -136(%rbp), %rdi        # 8-byte Reload
movq    -152(%rbp), %rsi        # 8-byte Reload
callq   command::command(std::function<void ()> const&)

который представляет собой просто пучок адресов стека из базового указателя, который передается функции привязки перед вызовом конструктора.

Все было бы по-другому, если бы вы на самом деле пытались использовать объект до его построения (вещи могут оказаться сложными с виртуальными таблицами функций).

Sidenote: это НЕ гарантированно будет безопасным, если вы копируете или передаете по значению объект и выходите из области видимости (и сохраняете адрес в месте расположения стека). Кроме того: если компилятор решает сохранить его (по какой-либо архитектуре/причине) в качестве смещения от базового фрейма, вы, вероятно, находитесь в undefined behaviorland.

Ответ 2

Да, вы можете использовать указатель на объект после его объявления (и, следовательно, иметь выделенное хранилище), но еще не инициализирован. Доступ к самому объекту (кроме очень ограниченных способов) дает поведение undefined; но вы этого не делаете. Это описано в С++ 11 3.8/5:

До того, как было запущено время жизни объекта, но после того, как было выделено хранилище, которое будет занимать объект [...], может использоваться любой указатель, который ссылается на место хранения, в котором находится или находился объект, но только в ограниченные пути. [...] Использование указателя, как если бы указатель имел тип void*, четко определен.

Вы просто передаете его в bind, который копирует значение указателя в обертку связанной функции, которая считается используемой, как если бы она была void*.