c++ - 英語 - 鼻から悪魔




C++の後置式の未定義と不特定の動作 (4)

サブ式が評価される順序は不特定であり、&&、||、? と "、"。

コンパイラは、関数プロトタイプのmyLambdaと戻り値のラムダ(戻り値の型から抽出されたもの)の両方を認識します。 私の最初の文は、彼が最初に評価した式であるので、コンパイラは自由です。 なぜなら、追加の副作用を持つon式の関数を決して呼び出すべきではないということです。

事前にお詫び申し上げます、私は評価秩序の一般的な話題はすでにそれについて多くのSOの質問をしていた知っている。 しかし、それらを見て、私は何かを重複しているとは思わないいくつかの具体的な点を明確にしたいと思います。 次のコードがあるとします。

#include <iostream>

auto myLambda(int& n)
{
    ++n;
    return [](int param) { std::cout << "param: " << param << std::endl; };
}

int main()
{
    int n{0};

    myLambda(n)(n);
    return 0;
}

上記のプログラムはコンパイル時に "n:0"を出力します。 ここでは、指定されていない順序付けがあります。異なる評価順序が行われた「n:1」と同じように簡単に出力できます。

私の質問は次のとおりです:

  1. 上記の最後の関数呼び出し(すなわちラムダ式呼び出し)、後置式myLambda(0) 、その引数n 、および後続の関数呼び出し自体の間で、シーケンシング関係は正確に何か?

  2. 上記は、 未定義または不特定の動作の例ですか?なぜ(正確には、標準を参照して)動作していますか?

  3. ラムダコードを[](int param) { std::cout << "hello" << std::endl }に変更した場合(つまり、結果が[](int param) { std::cout << "hello" << std::endl }ように、上記の2)に対する回答は依然として同じでしょうか?

編集 :lambdaパラメータ名を 'n'から 'param'に変更しました。混乱の原因と思われたからです。


ラムダの定義におけるnは、ラムダが定義する関数の正式な引数です。 それはまた、 nという名前のmyLambdaへの引数とは関連していません。 ここでの動作は、これらの2つの関数が呼び出される方法によって完全に決まります。 myLambda(n)(n)では、2つの関数引数の評価順序は不明です。 ラムダは、コンパイラに応じて、0または1の引数で呼び出すことができます。

もしラムダが[=n]()...定義されていたら、それは異なった振る舞いをします。


私は標準への適切な参照を見つけることはできませんでしたが、 hereで質問された引数の評価順序と同様の振る舞いを持ち、関数の引数の評価の順序は標準によって定義されていません。

5.2.2関数呼び出し

8 [ 注: postfix式と引数式の評価は、すべて互いに順序付けされていません。 引数式評価のすべての副作用は、関数が入力される前に順序付けされます(1.9参照)。 -end note ]

それでは、別のコンパイラの呼び出しの中でどのようになるのかを以下に示します。

#include <iostream>
#include <functional>

struct Int
{
    Int() { std::cout << "Int(): " << v << std::endl; }
    Int(const Int& o) { v = o.v; std::cout << "Int(const Int&): " << v << std::endl; }
    Int(int o) { v = o; std::cout << "Int(int): " << v << std::endl; }
    ~Int() { std::cout << "~Int(): " << v << std::endl; }
    Int& operator=(const Int& o) { v = o.v; std::cout << "operator= " << v << std::endl; return *this; }

    int v;
};

namespace std
{
    template<>
    Int&& forward<Int>(Int& a) noexcept
    {
        std::cout << "Int&: " << a.v << std::endl;
        return static_cast<Int&&>(a);
    }

    template<>
    Int&& forward<Int>(Int&& a) noexcept
    {
        std::cout << "Int&&: " << a.v << std::endl;
        return static_cast<Int&&>(a);
    }
}

std::function<void(Int)> myLambda(Int& n)
{
    std::cout << "++n: " << n.v << std::endl;
    ++n.v;
    return [&](Int m) { 
        std::cout << "n: " << m.v << std::endl;
    };
}

int main()
{
    Int n(0);

    myLambda(n)(n);
    return 0;
}

GCC g++ -std=c++14 -O2 -Wall -pedantic -pthread main.cpp && ./a.outおよびMSVC

Int(int):0
Int(const Int&):0
++ n:0
Int&:0
Int&:0
Int(const Int&):0
n:0
〜Int():0
〜Int():0
〜Int():1

したがって変数を作成し、返されたlambaに渡します。

Clang clang++ -std=c++14 main.cpp && ./a.out

Int(int):0
++ n:0
Int(const Int&):1
Int&:1
Int&:1
Int(const Int&):1
n:1
〜Int():1
〜Int():1
〜Int():1

ここで変数評価関数を作成し、次にlambaをコピーします。

評価の順序は次のとおりです。

struct A
{
    A(int) { std::cout << "1" << std::endl; }
    ~A() { std::cout << "-1" << std::endl; }
};

struct B
{
    B(double) { std::cout << "2" << std::endl; }
    ~B() { std::cout << "-2" << std::endl; }
};

void f(A, B) { }

int main()
{
    f(4, 5.);
}

MSVCおよびGCC:

2
1
-1
-2

クラング:

1
2
-2
-1

clang orderと同様にforwardであり、関数の引数の評価後にlambdaへの引数が渡されます


関数呼び出し(myLambda(n))(n)の後置式(余分な括弧のペアを与えた式)は、引数式n (右端の式)に対して順序付けされていないため、振る舞いは未定義です。 ただし、 nの変更は関数呼び出しmyLambda(n)内部で行われるため、未定義の動作はありません。 唯一の明確なシーケンシング関係は、実際にラムダを呼び出す前に、 myLambda(n)とfinal n両方を評価することは明らかです。 最後の質問では、ラムダがその引数を完全に無視することを選択した場合、その動作はもはや不特定ではありません:パラメータとして渡される値が指定されていませんが、どちらにも観察可能な違いはありません。





order-of-evaluation