2

Creating a temporary char buffer as a default function argument and binding an r-value reference to it allows us to compose statements on a single line whilst preventing the need to create storage on the heap.

const char* foo(int id, tmp_buf&& buf = tmp_buf()) // buf exists at call-site

Binding a reference/pointer to the temporary buffer and accessing it later yields undefined behaviour, because the temporary no longer exists.

As can be seen from the example app below the destructor for tmp_buf is called after the first output, and before the second output.

My compiler (gcc-4.8.2) doesn't warn that I'm binding a variable to a temporary. This means that using this kind of micro-optimisation to use an auto char buffer rather than std::string with associated heap allocation is very dangerous.

Someone else coming in and capturing the returned const char* could inadvertently introduce a bug.

1. Is there any way to get the compiler to warn for the second case below (capturing the temporary)?

Interestingly you can see that I tried to invalidate the buffer - which I failed to do, so it likely shows I don't fully understand where on the stack tmp_buf is being created.

2. Why did I not trash the memory in tmp_buf when I called try_stomp()? How can I trash tmp_buf?

3. Alternatively - is it safe to use in the manner I have shown? (I'm not expecting this to be true!)

code:

#include <iostream>

struct tmp_buf
{
    char arr[24];
    ~tmp_buf() { std::cout << " [~] "; }
};

const char* foo(int id, tmp_buf&& buf = tmp_buf())
{
    sprintf(buf.arr, "foo(%X)", id);
    return buf.arr;
}

void try_stomp()
{
    double d = 22./7.;
    char buf[32];
    snprintf(buf, sizeof(buf), "pi=%lf", d);
    std::cout << "\n" << buf << "\n";
}

int main()
{
    std::cout << "at call site: " << foo(123456789);
    std::cout << "\n";

    std::cout << "after call site: ";
    const char* p = foo(123456789);

    try_stomp();

    std::cout << p << "\n";
    return 0;
}

output:

at call site: foo(75BCD15) [~] 
after call site:  [~] 
pi=3.142857
foo(75BCD15)
Steve Lorimer
  • 27,059
  • 17
  • 118
  • 213
  • A good question, though possibly duplicate or possibly related to http://stackoverflow.com/questions/2506793/c-life-span-of-temporary-arguments – paisanco Nov 20 '14 at 23:10
  • I'm well aware that "Temporary objects are destroyed at the end of the full expression they're part of" as is stated in the answer to the question you linked to and also in my question above. I'm looking for clarification on other points – Steve Lorimer Nov 20 '14 at 23:14
  • Fair enough. Just one user's take. – paisanco Nov 20 '14 at 23:15
  • Sorry don't mean to sound harsh! :) – Steve Lorimer Nov 20 '14 at 23:20
  • 1
    Just confirming the issue: http://coliru.stacked-crooked.com/a/4bffdb7a4d94f0a4 – Marco A. Nov 20 '14 at 23:25
  • 1
    @SteveLorimer as for "Is there any way to get the compiler to warn for the second case below (capturing the temporary)" the answer is no. Because you aren't capturing a temporary, you are copying the implicit conversion of a member of a temporary. The compiler does not trace where the copied `char *` comes from (the `char[24]` is implicitly converted to a `char *` and that is copied if I wasn't clear). – PeterT Nov 20 '14 at 23:27
  • Also, you did "trash it", but you did not overwrite it. You can check `const char* p = foo(123456789);foo(1);std::cout << p << "\n";` for what I mean – PeterT Nov 20 '14 at 23:30
  • @MarcoA. `-Wreturn-stack-address` is, I guess, half way there. In the instance of using the returned the stack address inside the currently evaluating expression is valid and allowable. However, using that address outside of the currently evaluating expression is not - Boo that the compiler can't warn on that. I suppose only possible to pick that up with static analysis or something? Thanks for the coliru! – Steve Lorimer Nov 20 '14 at 23:40
  • @PeterT Thanks for the explanation - I guess that is what I was looking for. Bummer for my hacky micro optimisation! :/ – Steve Lorimer Nov 20 '14 at 23:41
  • @PeterT if you care to put your 2 comments into an answer I'll accept that – Steve Lorimer Nov 20 '14 at 23:47

1 Answers1

1

For question 2.

The reason you didn't trash the variable is that the compile probably allocated all the stack space it needed at the start of the function call. This includes all the stack space for the temporary objects, and objects that are declared inside a nested scope. You can't guarantee that the compiler does this (I think), rather than push objects on the stack as needed, but it is more efficient and easier to keep track of where your stack variables are this way.

When you call the try_stomp function, that function then allocates its stack after (or before, depending on your system) the stack for the main function.

Note that the default variables for a function call are actually by the compile to the calling code, rather than being part of the called function (which is why the need to be part of the function declaration, rather than the definition, if it was declared separately).

So your stack when in try_stomp looks something like this (there is a lot more going on in the stack, but these are the relevant parts):

main       - p
main       - temp1
main       - temp2
try_stomp  - d
try_stomp  - buf

So you can't trash the temporary from try_stomp, at least not without doing something really outrageous.

Again, you can't rely on this layout, as it is compile dependent, and is just an exmaple of how the compiler might do it.

The way to trash the temporary buffer would be to do it in the destructor of tmp_buf.

Also interestingly, MSVC seems to allocate stack space for all of the temporary objects separately, rather than re-use the stack space for both objects. This means that even repeated calls to foo won't trash each other. Again, you can't depend on this behavior (I think - I couldn't find an reference to it).

For question 3. No, don't do this!

The Dark
  • 8,453
  • 1
  • 16
  • 19
  • 1
    + 1 for the **No, don't do this!** :) – Steve Lorimer Nov 20 '14 at 23:55
  • Re the stack storage allocation, if I create a buffer local to main `char buf[24]` *after* `const char* p = foo(123456789);`, and write to it, it stomps all over the address `p` points to. – Steve Lorimer Nov 20 '14 at 23:57
  • 1
    That would probably be because the compiler is re-using the stack space that was used for the temporary for the `buf` variable, but it is doing this because they are all in the same function. Note: it doesn't stomp over it in MSVC. – The Dark Nov 21 '14 at 00:02