4

Consider the following program (and its alternative in the comment) in C++17:

#include<iostream>

void a(int) {
    std::cout << "a\n";
}

void b(int) {
   std::cout << "b\n";
}

int main() {
    using T = void(*)(int);

    T f = a;
    (T(f))((f=b,0)); // alternatively: f((f=b,0))
}

With -O2 option, Clang 9.0.0 prints a and GCC 9.2 prints b. Both warn me about unsequenced modification and access to f. See godbolt.org.

My expectation was that this is program has well-defined behavior and will print a, because C++17 guarantees that the left-hand expression of the call (T(f)) is sequenced before any evaluation of the arguments. Because the result of the expression (T(f)) is a new pointer to a, the later modification of f should have no impact on the call at all. Am I wrong?

Both compilers give the same output if I use f((f=b,0)); instead of (T(f))((f=b,0));. Here I am slightly unsure about the undefined behavior aspect. Would this be undefined behavior because f still refers to the declared function pointer after evaluation, which will have been modified by the evaluation of the arguments and if so, why exactly would that cause undefined behavior rather than calling b?

I have asked a related question with regards to order of evaluation of non-static member function calls in C++17 here. I am aware that writing code like this is dangerous and unnecessary, but I want to understand the details of the C++ standard better.

Edit: GCC trunk now also prints a after the bug filed by Barry (see his answer below) has been fixed. Both Clang and GCC trunk do still show false-positive warnings with -Wall, though.

walnut
  • 21,629
  • 4
  • 23
  • 59
  • People didn't make a very thorough check that compilers actually implement all the new order of evaluation rules. If you keep experimenting with this, use the latest development version of the compiler, and be ready to file bugs. – Marc Glisse Oct 02 '19 at 16:35

1 Answers1

2

The C++17 rule is, from [expr.call]/8:

The postfix-expression is sequenced before each expression in the expression-list and any default argument. The initialization of a parameter, including every associated value computation and side effect, is indeterminately sequenced with respect to that of any other parameter.

In (T(f))((f=b,0));, (T(f)) is sequenced before the initialization of the parameter from (f=b, 0). All of this is well-defined and the program should print "a". That is, it should behave just like:

auto __tmp = T(f);
__tmp((f=b, 0));

The same is true even if we change your program such that this were valid:

T{f}(f=b, 0); // two parameters now, instead of one

The f=b and 0 expressions are indeterminately sequenced with each other, but T{f} is still sequenced before both, so this would still invoke a.

Filed 91974.

Barry
  • 286,269
  • 29
  • 621
  • 977
  • Thank you for the answer. `T(f)(f=b, 0);` doesn't work because it will be considered a redefinition of `f`. I was also really interested in the case `f((f=b, 0))`. Is there something different happening in that case? – walnut Oct 02 '19 at 16:19
  • Until C++17, the individual arguments and the postfix-expression were all of them unsequenced. – Deduplicator Oct 02 '19 at 16:52
  • The `T(f)` has nothing to do with it: `f((f=b,0))` has the same meaning (since C++17, again). – Davis Herring Oct 06 '19 at 02:00