Почему параметр lambda auto& выбирает константную перегрузку?

Я пытаюсь реализовать класс, который обертывает произвольный тип и мьютекс. Чтобы получить доступ к обернутым данным, необходимо передать объект функции в качестве параметра метода 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& в порядке.

Почему это ? И есть ли способ заставить его работать должным образом при использовании общей лямбды?


person Unda    schedule 01.03.2019    source источник
comment
@YSC Лямбда, общая или иная, является классом, а не шаблоном класса. F счастливо разрешается этому классу. Тот факт, что у класса есть шаблоны членов, в этот момент не имеет значения.   -  person Igor Tandetnik    schedule 01.03.2019


Ответы (1)


Это основная проблема, связанная с тем, что происходит с вызываемыми объектами, недружественными к SFINAE. Дополнительные сведения см. на странице P0826. .

Проблема в том, что когда вы вызываете это:

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

У нас есть две перегрузки locked, и мы должны попробовать обе. Перегрузка без 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 — нам просто нужно убедиться, что мы можем определить тип возвращаемого значения без создания экземпляра тела.


Более тщательным языковым решением было бы разрешить «вывод this» (см. раздел в документе, посвященный этой конкретной проблеме). Но этого не будет в C++20.

person Barry    schedule 01.03.2019
comment
Ну это позор. +1, так как это заставило меня действительно почесать голову. Я не мог понять, почему он вызывает версию const, и, как вы указываете, это действительно не так, он просто должен проверить, и проверка приводит к серьезной ошибке. - person NathanOliver; 01.03.2019
comment
Спасибо за это объяснение. Не могли бы вы подробнее остановиться на Тело находится вне непосредственного контекста? (мне неясно, что именно является непосредственным контекстом) - person YSC; 01.03.2019
comment
Спасибо за пояснение и обходной путь. Это работает, но так как это более подробно, чем написание типа, я сделаю это вместо использования auto для этого случая. Бумагу тоже посмотрю. - person Unda; 01.03.2019
comment
Будет ли это работать, если вы возьмете параметр как параметр auto&& (хотя тогда вы можете передать значения r, которые вы действительно не хотите разрешать)? - person Nicol Bolas; 01.03.2019
comment
@NicolBolas Нет, та же проблема. Вы должны структурировать лямбду таким образом, чтобы перегрузка была отклонена при создании экземпляра оператора вызова, а не тела. - person Barry; 01.03.2019
comment
@ Барри, я вижу в начале вашей статьи, что этот случай можно решить с помощью функции, не являющейся членом, с шаблоном для друзей. - person Unda; 02.03.2019
comment
@Unda Да, если вы хотите вместо этого называть это locked(str, []{...}). Просто меняет синтаксис. - person Barry; 02.03.2019
comment
@Barry, я думаю, что это отличная альтернатива, пока P0826 не станет стандартом: он работает с auto&, как и следовало ожидать, и позволяет избежать дублирования кода. - person Unda; 02.03.2019
comment
@Unda Вы имеете в виду P0847? - person Barry; 02.03.2019
comment
@ Барри, да, я думал, ты дважды упомянул одну и ту же статью в своем ответе, моя ошибка. - person Unda; 02.03.2019
comment
Причина, по которой мы должны создавать экземпляр тела лямбды во время O/R, заключается в выведенном типе возвращаемого значения. Достаточно явно указать -> void в лямбде; нет необходимости делать это полностью SFINAE-правильным. - person T.C.; 03.03.2019
comment
@Т.С. Вы правы - соединил две совершенно не связанные между собой вещи. - person Barry; 03.03.2019
comment
@Т.С. Почему он создает экземпляр лямбда, хотя тип возвращаемого значения вообще не зависит от типа параметра лямбда? Это всегда пустота. - person Johannes Schaub - litb; 15.06.2019
comment
@Johannes Вы знаете, что он всегда пуст, только создав его экземпляр. Если вы не предлагаете специальный корпус, лексически не содержащий операторов return, что довольно специфично. - person Barry; 15.06.2019
comment
Компиляторы @Barry делают много синтаксических специальных регистров в шаблонах, чтобы избавить пользователей от бремени написания typename. Почему бы не сделать это для возвращаемых типов? Если в лямбде мы напишем return e, и будет ли это ранее объявленное int e; или void()-выражение, то компилятор может пометить возвращаемый тип шаблона как независимый и сразу вывести его к этому типу. - person Johannes Schaub - litb; 16.06.2019