Order of evaluation in application/tuple/...?

Yes and no. You are right in that the C standard does not put sequence points between function argument evaluations. But since the C standard puts sequence points before function calls, it guarantees that, in the expression f(g(), h()), the execution of g and h will not be interleaved (though you do not know which one happens first).

Ah, that’s interesting! (I think it is not enough to say that a function call is always sequenced after its arguments, but the text of the standard (that I just checked) goes further and effectively requires the body of the called function not to be interleaved with anything else:)

§6.5.2.2
10. There is a sequence point after the evaluations of the function designator and the actual arguments but before the actual call. Every evaluation in the calling function (including other function calls) that is not otherwise specifically sequenced before or after the execution of the body of the called function is indeterminately sequenced with respect to the execution of the called function.94)
94) In other words, function executions do not ‘‘interleave’’ with each other.

It rules out many interleavings but not all of them, though (f(i++, i++)).

There certainly can be interleaving in C and C++. This:

do_it (wrap_it (make_1 ()),
      (wrap_it (make_2 ()));

can proceed by (1) calling make_1(), (2) calling make_2(), (3) call wrap_it() for argument 2, (4) call wrap_it() for argument 1, before applying do_it(). Or any combination therefore provided dependencies are respected. It could also parallelize them if no observable effects can be proved (which generally it can’t).

That is more problematic with C++ than C because C++ uses destructors for resource management. So this is broken in C++:

do_it(std::unique_ptr<T>(make_1()),
      std::unique_ptr<T>(make_2()));

because after calling make_1() the compiler could, before initializing its unique_ptr object, call make_2(), and if make_2() raises an exception there is a memory leak.

Obviously that particular case is not an issue for ocaml because it uses garbage collection. To what extent such a wild west state of affairs is desirable is a matter for debate.

Edit: after reading the other follow-up posts this may be wrong for C, but I am pretty certain it is right for C++. There was a proposal to deal with it which was rejected.

Edit2: I think this particular interleaving is permissible in C.

1 Like

Indeed, this is a famous one, and why C++ programmers are told to use std::make_unique or creation functions that directly return unique_ptr (which will be correct following to @silene’s remark). When unique_ptr was introduced, a lot of hair-pulling bugs like this disappeared, assuming it was properly used to get rid of owned raw pointers. In your example, one should clarify that make_1 and make_2 return owned raw pointers (correct me if I misunderstood).

I think this example can also be an issue in OCaml (independently of whether evaluation order was defined or not, in fact). Thanks to the GC this is not a problem for memory allocation of course, but for any kind of resource that requires to call a clean-up function (and more often than not a finaliser is not the right answer), there are many subtle bugs that can happen, especially if you have several resources, and especially if their lifetimes do not match. In fact in current OCaml I believe that one would be forced to reason about which functions possibly raise, and program very defensively. In your C++ example, only the undefined evaluation order is problematic, thanks to the automatic management provided by unique_ptr.

@gasche mentions that it would be better to define a left-to-right order once and for all (if one was allowed to start from scratch again). The evaluation order has in several occasions been fixed to be consistent across backends (in favour of right-to-left order). So even if it might be too late to change in favour of a left-to-right order, I have the impression that it would not be unrealistic that the order ends up being defined (it might even already be the case in practice or a goal in some maintainer’s mind).

1 Like

That is exactly right. But, in a way, that is also an understatement. Indeed, this issue was actually the motivation for adding make_unique to C++ in the first place. Quoting from the original proposal to the C++ committee,

make_unique's presence in the Standard Library will have several wonderful
consequences. […] make_unique prevents the unspecified-evaluation-order leak triggered by expressions like foo(unique_ptr<X>(new X), unique_ptr<Y>(new Y)). (Following the advice "never say new" is simpler than "never say new, unless you immediately give it to a named unique_ptr".)