web-dev-qa-db-ja.com

lambda auto&パラメータがconstオーバーロードを選択するのはなぜですか?

任意の型とミューテックスをラップするクラスを実装しようとしています。ラップされたデータにアクセスするには、lockedメソッドのパラメーターとして関数オブジェクトを渡す必要があります。ラッパークラスは、ラップされたデータをパラメーターとしてこの関数オブジェクトに渡します。

ラッパークラスをconstおよびnon-constで動作させたいので、次のことを試しました

#include <mutex>
#include <string>

template<typename T, typename Mutex = std::mutex>
class   Mutexed
{
private:
    T m_data;
    mutable Mutex m_mutex;

public:
    using type = T;
    using mutex_type = Mutex;

public:
    explicit Mutexed() = default;

    template<typename... Args>
    explicit Mutexed(Args&&... args)
        : m_data{std::forward<Args>(args)...}
    {}

    template<typename F>
    auto locked(F&& f) -> decltype(std::forward<F>(f)(m_data)) {
        std::lock_guard<Mutex> lock(m_mutex);
        return std::forward<F>(f)(m_data);
    }

    template<typename F>
    auto locked(F&& f) const -> decltype(std::forward<F>(f)(m_data)) {
        std::lock_guard<Mutex> lock(m_mutex);
        return std::forward<F>(f)(m_data);
    }
};

int main()
{
    Mutexed<std::string> str{"Foo"};

    str.locked([](auto &s) { /* this doesn't compile */
        s = "Bar";
    });

    str.locked([](std::string& s) { /* this compiles fine */
        s = "Baz";
    });
    return 0;
}

一般的なラムダを使用した最初のlocked呼び出しは、次のエラーでコンパイルに失敗します

/home/foo/tests/lamdba_auto_const/lambda_auto_const/main.cpp: In instantiation of ‘main()::<lambda(auto:1&)> [with auto:1 = const std::__cxx11::basic_string<char>]’:
/home/foo/tests/lamdba_auto_const/lambda_auto_const/main.cpp:30:60:   required by substitution of ‘template<class F> decltype (forward<F>(f)(((const Mutexed<T, Mutex>*)this)->Mutexed<T, Mutex>::m_data)) Mutexed<T, Mutex>::locked(F&&) const [with F = main()::<lambda(auto:1&)>]’
/home/foo/tests/lamdba_auto_const/lambda_auto_const/main.cpp:42:6:   required from here
/home/foo/tests/lamdba_auto_const/lambda_auto_const/main.cpp:41:11: error: passing ‘const std::__cxx11::basic_string<char>’ as ‘this’ argument discards qualifiers [-fpermissive]
         s = "Bar";
           ^
In file included from /usr/include/c++/5/string:52:0,
                 from /usr/include/c++/5/stdexcept:39,
                 from /usr/include/c++/5/array:38,
                 from /usr/include/c++/5/Tuple:39,
                 from /usr/include/c++/5/mutex:38,
                 from /home/foo/tests/lamdba_auto_const/lambda_auto_const/main.cpp:1:
/usr/include/c++/5/bits/basic_string.h:558:7: note:   in call to ‘std::__cxx11::basic_string<_CharT, _Traits, _Alloc>& std::__cxx11::basic_string<_CharT, _Traits, _Alloc>::operator=(const _CharT*) [with _CharT = char; _Traits = std::char_traits<char>; _Alloc = std::allocator<char>]’
       operator=(const _CharT* __s)
       ^

ただし、std::string&パラメータを使用した2番目の呼び出しは問題ありません。

何故ですか ?そして、一般的なラムダを使用しているときに期待どおりに機能させる方法はありますか?

25
Unda

これは、SFINAEに対応していない呼び出し可能オブジェクトで何が起こるかということに関して根本的に問題です。詳細については、 P0826 をご覧ください。

問題は、これを呼び出すと:

_ str.locked([](auto &s) { s = "Bar"; });
_

lockedtwoオーバーロードがあり、両方を試す必要があります。非constオーバーロードは正常に機能します。しかし、constは、とにかくオーバーロードの解決で選択されない場合でも、インスタンス化する必要があります(これは一般的なラムダなので、decltype(std::forward<F>(f)(m_data))が何であるかを理解するにはそれをインスタンス化するには)、そのインスタンス化はラムダの本体内で失敗します。本文は直接のコンテキストの外にあるため、置換の失敗ではなく、ハードエラーです。

これを呼び出すと:

_str.locked([](std::string& s) { s = "Bar"; });
_

オーバーロード解決のプロセス全体で本体をまったく見る必要はありません。呼び出しサイトで単に拒否できます(_const string_を_string&_に渡すことができないため)。

今日の言語では、この問題に対する実際の解決策はありません。基本的には、ラムダに制約を追加して、インスタンス化の失敗が本体ではなく置換の直接のコンテキストで発生するようにする必要があります。何かのようなもの:

_str.locked([](auto &s) -> void {
    s = "Bar";
});
_

これをSFINAEに対応させる必要はないことに注意してください。本体をインスタンス化せずに戻り値の型を判別できることを確認する必要があるだけです。


より完全な言語ソリューションは、 "Deducing this"(この特定の問題に関する論文の the section を参照)を許可することでした。しかし、それはC++ 20にはありません。

32
Barry