13

I'm storing an action that needs to operate on an object but I don't want to use inheritance. So the technique I'm using is to have a non-member function that accepts a pointer to an object and then store it in the object, like so:

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;
}

My question is it safe to do command com(std::bind(test_action, &com));? Or is it undefined behavior?

user4085715
  • 395
  • 2
  • 8
  • As long as you don't actually do anything with the object before it's constructed, you are golden. And as @MarcoA commented, you are only using the address before that time. – Deduplicator Sep 27 '14 at 11:59
  • It might be a good idea to make copying and moving protected if you intend to do this often. – Luc Danton Sep 27 '14 at 12:15

2 Answers2

7

First off: what is an object?

[intro.object]\1

[...] An object is a region of storage [...]

The storage is allocated before the lifetime of an object starts:

[basic.life]

Before the lifetime of an object has started but after the storage which the object will occupy has been allocated [..] any pointer that refers to the storage location where the object will be or was located may be used but only in limited ways. For an object under construction or destruction, see 12.7 [construction and destruction]. Otherwise, such a pointer refers to allocated storage (3.7.4.2), and using the pointer as if the pointer were of type void*, is well-defined.

Thus the pointer refers to allocated space and there's no harm in using it. You're just asking for a stack address and any compiler should be able to figure it out correctly. No initialization operation by the object itself is required in this specific instance.

This makes sense since in a classic AST-fashion compiler if you to take a look at the standard hierarchy for declarators, in a simple toy-code like

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

int funct(command*) {
    return 2;
}

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

the line

command com(funct(&com));

is interpreted as follows:

[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

And finally for your code this is how gcc compiles this line (-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&)

which is: just a bunch of stack addresses from the base pointer which get passed to the binding function before invoking the constructor.

Things would be different if you actually tried to use the object before its construction (things might get tricky with virtual function tables).

Sidenote: this is NOT guaranteed to be safe if you're copying around or passing by value the object and going out-of-scope (and still keeping an address to the stack location). Also: if the compiler decides to store it (for whatever architecture/reason) as an offset from the base frame, you're probably off to undefined behaviorland.

Marco A.
  • 43,032
  • 26
  • 132
  • 246
  • 1
    +1, even though arguing from happenstance is a good way to go completely wrong. – Deduplicator Sep 27 '14 at 12:07
  • I agree and I usually don't base any observation from "it compiles" or "it works". Undefined behavior is a tricky beast. Anyway I see no harm in this case. If I missed something please let me know and I'll edit the post – Marco A. Sep 27 '14 at 12:09
  • @OliverCharlesworth just added some details – Marco A. Sep 27 '14 at 12:23
  • 6
    What I mean is, all the assembler proves is that this particular compiler generated "safe" code in this particular instance. It doesn't help at all with the question of whether this is UB. What is required is a quote from the standard (or equivalent) that explains that `&com` is a valid pointer in this scenario. – Oliver Charlesworth Sep 27 '14 at 12:25
  • 1
    Shameless plug: http://stackoverflow.com/questions/25754796/may-i-call-a-virtual-function-to-initialize-a-base-class-sub-object/25755113#25755113 – Deduplicator Sep 27 '14 at 19:02
3

Yes, you can use a pointer to an object once it's been declared (and so has storage allocated) but not yet initialised. Accessing the object itself [except in very restricted ways] gives undefined behaviour; but you don't do that. This is described by C++11 3.8/5:

Before the lifetime of an object has started but after the storage which the object will occupy has been allocated [...] any pointer that refers to the storage location where the object will be or was located may be used but only in limited ways. [...] Using the pointer as if the pointer were of type void*, is well-defined.

You simply pass it to bind, which copies the pointer value into the bound function wrapper, which counts as using it as if it were void*.

Mike Seymour
  • 249,747
  • 28
  • 448
  • 644