2

Suppose I have a function foo(x, y, z) that is invariant w.r.t. all permutations of its arguments. I also have an iterator it, such that the iterators it, it + 1 and it + 2 can be dereferenced.

Is it fine to write

... = foo(*it++, *it++, *it++);  // (1)

instead of

... = foo(*it, *(it + 1), *(it + 2));  // (2)

?

As far as I understand, technically it is correct since C++17 due to (quoting cppreference.com and taking into account that it can be a raw pointer)

15) In a function call, value computations and side effects of the initialization of every parameter are indeterminately sequenced with respect to value computations and side effects of any other parameter.

The order of evaluation of function arguments is not defined, but for foo() the order doesn't matter.

But is it an acceptable coding style? On the one hand, (1) is nicely symmetric and implies that foo has such an invariance, and (2) looks somewhat ugly. On the other hand, (1) immediately raises questions about its correctness - the one who reads the code should check the description or definition of foo to verify the correctness of the call.

If the body of foo() is small and the invariance is obvious from the function definition, would you accept (1)?

(Probably, this question is opinion-based. But I can't help but ask it.)

Evg
  • 25,259
  • 5
  • 41
  • 83
  • 1
    Related question : https://stackoverflow.com/questions/38501587/what-are-the-evaluation-order-guarantees-introduced-by-c17 – François Andrieux Sep 20 '18 at 17:52
  • 9
    "But is it an acceptable coding style?" No. – Barry Sep 20 '18 at 17:52
  • [this](https://stackoverflow.com/questions/28816936/is-the-value-of-expression-f-g-when-f-g-modify-same-global-variable-und) has the answer but I'm not sure if I want to close it as a dupe. – NathanOliver Sep 20 '18 at 17:53
  • 2
    To elaborate on @Barry's comment, even if it's legal and works, the fact that it raises such a complex question is a good indication that it shouldn't be seen in production code. You want your code to be obvious and understandable, which this clearly wouldn't be, so this wouldn't be "an acceptable coding style". – François Andrieux Sep 20 '18 at 17:53
  • 1
    It's not only because it raises questions to whoever reads it. The fact that this line of code will break when its passed to a C++14 compiler is also a big issue. If the rest of the code base is not reliant on C++17 features it can be easy to make such a mistake. – patatahooligan Sep 20 '18 at 17:55
  • Furthering @Barry comment see [Does this code from “The C++ Programming Language” 4th edition section 36.3.6 have well-defined behavior?](https://stackoverflow.com/q/27158812/1708801) for the dangers of overly complex code. It also covers a lot of standards ground that is related to this. – Shafik Yaghmour Sep 20 '18 at 17:56
  • If your question applies __only__ to C++17, I think you should mention that in the title and/or lead part of the question. The tags are far from obvious. – Tim Randall Sep 20 '18 at 17:59
  • I doubt unspecified would be any better than undefined in this case. – Slava Sep 20 '18 at 18:02
  • 3
    @TimRandall It is mentioned in the body of the Q *As far as I understand, technically it is correct since C++17*. Also if the Q is tagged as C++17 it means it is talking about C++17. This is what the tag system is for. We don't want to put tags in titles – NathanOliver Sep 20 '18 at 18:03
  • Just *don't* rely on argument evaluation order and you'll be in a happier place. – Jesper Juhl Sep 20 '18 at 18:23

1 Answers1

6

You are correct that in C++17

foo(*it++, *it++, *it++);

is not undefined behavior. As your quote states, and as stated in [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.

each increment is sequenced so you do not have multiple unsequenced writes. The order of evaluation of function arguments is still unspecified in C++17 so that means you just have unspecified behavior (you can't know which element is passed to each argument).

As long as your function doesn't care about this then you are fine. If the order of the arguments will matter then you'll have to use your second version.


That all said I would prefer the use of

foo(*it, *(it + 1), *(it + 2));

even if the order doesn't matter. It makes the code backwards compatible and IMHO it is easier to reason about. I'd rather see a it += 3 in the increment part of a for loop then the multiple increments in the function call and no increment in the increment part of the loop.

NathanOliver
  • 171,901
  • 28
  • 288
  • 402
  • The first variant could be used with basic input-iterators, the alternative not so. – Deduplicator Sep 20 '18 at 18:24
  • @Deduplicator Since the OP said `foo(*it, *(it + 1), *(it + 2))` would work for them I'm assuming they don't have a input-iterator. – NathanOliver Sep 20 '18 at 18:25
  • 1
    Can you quote the part of the standard that states that side effects on each argument evaluation are completed before any other argument evaluation starts? (Ok if not, I'd just like to see it) – Yakk - Adam Nevraumont Sep 20 '18 at 18:31
  • 1
    @Yakk-AdamNevraumont I found the relevant section in the standard. – NathanOliver Sep 20 '18 at 18:42
  • The old wording (C++11) was *The evaluations of the postfix expression and of the arguments are all unsequenced relative to one another. All side effects of argument evaluations are sequenced before the function is entered* – NathanOliver Sep 20 '18 at 18:44
  • @NathanOliver So, there is an ambiguity in that sentence. Are the *components* of the initialization of a parameter (value computation and side effect) indepenently indeterminately sequenced, or is the bundle (both its value computation and side effect) grouped and then indeterminately sequenced? I believe the standard means the second? But extracting a hard gaurantee of the second from that paragraph is a stretch. – Yakk - Adam Nevraumont Sep 20 '18 at 19:08
  • @Yakk-AdamNevraumont To me it reads like the comma operator (A then B then C), just there is no defined order of evaluation (A or B or C then one one the two others then the last one) – NathanOliver Sep 20 '18 at 19:13
  • 1
    @NathanOliver See, I read some of the papers and commentary leading up to the change, so I know what they *meant* to say. I'm just saying I'm not sure how well they said it. ;) – Yakk - Adam Nevraumont Sep 20 '18 at 19:15
  • @Yakk-AdamNevraumont what is indeterminately sequenced are not the components but the evaluation (both side effect and value computation) of each argument relative to the others. the order in which value computation and side effect takes effect depends on the expression of arguments. – Jans Sep 20 '18 at 19:15
  • 1
    @Deduplicator: "*The first variant could be used with basic input-iterators, the alternative not so.*" No, it cannot. The `reference` returned by an input iterator cannot be used after the iterator it is associated with has been incremented. Or rather, InputIterator does not *require* that the `reference` returned remain valid after incrementing the iterator. `iostream` iterators do this all the time. Now, Forward/BidirectionalIterators *do* have that requirement, and they can't have integers added to them. – Nicol Bolas Sep 20 '18 at 20:55