drdilyor's blog

By drdilyor, history, 18 months ago, In English

Take a look at this innocent piece of code:

    vector<vector<int>> arr{
        {1, 2, 3, 4},
        {5, 6, 7, 8},
        {9, 10, 11, 12},
        {13, 14, 15, 16},
    };
    int n = arr.size();

    for (int i = 0; i < n/2; i++) {
        for (int j = 0; j < n/2; j++) {
            tie(arr[i][j], arr[j][n-i-1], arr[n-i-1][n-j-1], arr[n-j-1][i]) =
                {arr[j][n-i-1], arr[n-i-1][n-j-1], arr[n-j-1][i], arr[i][j]};
            //tie(arr[i][j], arr[j][n-i-1], arr[n-i-1][n-j-1], arr[n-j-1][i]) =
            //    tuple{arr[j][n-i-1], arr[n-i-1][n-j-1], arr[n-j-1][i], arr[i][j]};
        }
    }
    for (auto& i : arr) {
        for (int j : i) 
            cout << j << ' ';
        cout << '\n';
    }

Outputs (with optimizations disabled):

4 8 12 16 
3 7 11 15 
8 7 10 14 
4 3 9 13 

which is totally gibberish.

Using tuple{} instead of initializer list ({}) for tie() gives the correct answer. This behavior is observed both on GCC and clang.

In fact, this is present with code as simple as this:

int a = 1, b = 2;
tie(a, b) = {b, a};

This once again shows one of the dark sides of C++. Hope no one spends 30 mins searching for such bug after this blog :):

Tags c++
  • Vote: I like it
  • +78
  • Vote: I do not like it

»
18 months ago, # |
  Vote: I like it 0 Vote: I do not like it

omg same on my laptop why

»
18 months ago, # |
  Vote: I like it +33 Vote: I do not like it

Tuple creates a copy of the value, and the initializer list will be used to create a temporary object of type std::tuple<int&, int&, int&, int&>, same as the left hand side, as this is the best overload. Therefore it's the same as writing out four assignments, which is pretty much guaranteed to produce incorrect results. I'm not sure whether the order of assignments is well defined.

  • »
    »
    18 months ago, # ^ |
      Vote: I like it +6 Vote: I do not like it

    I visualized this behavior with a custom class:

    Wrapper

    Then, with the initializer-list option

    array<Wrapper, 3> X{0, 1, 2};
    tie(X[2], X[1], X[0]) = {X[0], X[1], X[2]};
    

    we will see

    default-construct: 0.
    default-construct: 1.
    default-construct: 2.
    copy-assign: 2 <- 0.
    copy-assign: 1 <- 1.
    copy-assign: 0 <- 0.
    

    which obviously gives wrong results. With the tuple method:

    array<Wrapper, 3> X{0, 1, 2};
    tie(X[2], X[1], X[0]) = tuple{X[0], X[1], X[2]};
    

    we see

    default-construct: 0.
    default-construct: 1.
    default-construct: 2.
    copy-construct: 2.
    copy-construct: 1.
    copy-construct: 0.
    move-assign: 2 <- 0.
    move-assign: 1 <- 1.
    move-assign: 0 <- 2.
    

    which must be correct.

    I also read at the end of this thread that tuple assignment ordering may be left up to the compiler to allow for earlier assignment of throwable types, but I failed to recreate this on any compilers I tried. Still, it may be good to assume that ordering is not guaranteed.

    Here is what I tried:

    Throwable & Noexcept classes
    Throwable A;
    Noexcept B;
    try {
    	tie(B, A) = tuple{B, A};
    } catch (...) {
    	cout << "Thrown & caught.\n";
    }
    try {
    	tie(A, B) = tuple{A, B};
    } catch (...) {
    	cout << "Thrown & caught.\n";
    }
    

    But on all compilers I had access to, I could only get the result

    [Noexcept] copy-assign.
    [Throwable] copy-assign.
    Thrown & caught.
    [Throwable] copy-assign.
    Thrown & caught.
    

    which is inconclusive.

»
18 months ago, # |
  Vote: I like it +33 Vote: I do not like it

Hope no one spends 30 mins searching for such bug after this blog

who ever code like this!? (except pythonists)

  • »
    »
    18 months ago, # ^ |
    Rev. 3   Vote: I like it +5 Vote: I do not like it

    Well, yeah you got me, I'm a pythonista undercover :)

    BTW I found a prettier (=hard to understand) method:

    arr[i][j] = exchange(arr[j][n-i-1],
                exchange(arr[n-i-1][n-j-1],
                exchange(arr[n-j-1][i],
                         arr[i][j])));
    
    • »
      »
      »
      18 months ago, # ^ |
        Vote: I like it +6 Vote: I do not like it

      I think this is what you're looking for

      #include <bits/stdc++.h>
      using namespace std;
      
      template<class H, class... T>
      void move_left(H& first, T&... other) {
          tie(first, other...) = tuple{other..., first};
      }
      
      int main() {
          int x = 1, y = 2, z = 3;
          move_left(x, y, z);
          cout << x << ' ' << y << ' ' << z << '\n';
      }