Подтвердить что ты не робот

Почему std:: lock_guard/std:: unique_lock не использует стирание типа?

Почему std::lock_guard и std::unique_lock необходимо указать тип блокировки в качестве параметра шаблона?

Рассмотрим следующий вариант. Во-первых, в пространстве имен detail существуют классы стирания типа (не шаблонный абстрактный базовый класс и производный от шаблона класс):

#include <type_traits>
#include <mutex>
#include <chrono>
#include <iostream>

namespace detail {

    struct locker_unlocker_base {
        virtual void lock() = 0;
        virtual void unlock() = 0;
    };

    template<class Mutex>
    struct locker_unlocker : public locker_unlocker_base {
        locker_unlocker(Mutex &m) : m_m{&m} {}
        virtual void lock() { m_m->lock(); }
        virtual void unlock() { m_m->unlock(); }
        Mutex *m_m;
    };
}

Теперь te_lock_guard, защита блокировки стираемого типа, просто размещайте-новость, объект с правильным типом при построении (без распределения динамической памяти):

class te_lock_guard {
public:
    template<class Mutex>
    te_lock_guard(Mutex &m) {
        new (&m_buf) detail::locker_unlocker<Mutex>(m);
        reinterpret_cast<detail::locker_unlocker_base *>(&m_buf)->lock();
    }
    ~te_lock_guard() {
        reinterpret_cast<detail::locker_unlocker_base *>(&m_buf)->unlock();
    }

private:
    std::aligned_storage<sizeof(detail::locker_unlocker<std::mutex>), alignof(detail::locker_unlocker<std::mutex>)>::type m_buf;
};

Я проверил производительность по сравнению со стандартными библиотечными классами:

int main() {
    constexpr std::size_t num{999999};
    {
        std::chrono::steady_clock::time_point begin = std::chrono::steady_clock::now();
        for(size_t i = 0; i < num; ++i) {
            std::mutex m;
            te_lock_guard l(m);
        }
        std::chrono::steady_clock::time_point end= std::chrono::steady_clock::now();
        std::cout << std::chrono::duration_cast<std::chrono::microseconds>(end - begin).count() << std::endl;
    }
    {
        std::chrono::steady_clock::time_point begin = std::chrono::steady_clock::now();
        for(size_t i = 0; i < num; ++i) {
            std::mutex m;
            std::unique_lock<std::mutex> l(m);
        }
        std::chrono::steady_clock::time_point end= std::chrono::steady_clock::now();
        std::cout << std::chrono::duration_cast<std::chrono::microseconds>(end - begin).count() << std::endl;
    }
}

Используя g++ с -O3, статистически значительная потеря производительности отсутствует.

4b9b3361

Ответ 1

Поскольку это усложняет реализацию без существенных преимуществ и скрывает тот факт, что std::lock_guard и std::unique_lock знают о типе блокировки, которую они охраняют во время компиляции.

Ваше решение является обходным путем для того факта, что вычитание параметра шаблона шаблона не происходит во время построения - это рассматривается в предстоящем стандарте.

Необходимость указывать тип блокировки - это раздражающий шаблон, который будет разрешен в С++ 17 (не только для защиты от блокировки) благодаря Вывод параметра шаблона для конструкторы (P0091R3).

Предложение (которое было принято), позволяет выводить параметры шаблона из конструкторов, устраняя необходимость в вспомогательных функциях make_xxx(...) или явно указывая имена типов, которые компилятор должен уметь вывести:

// Valid C++17
for(size_t i = 0; i < num; ++i) {
    std::mutex m;
    std::unique_lock l(m);
}

Ответ 2

Roll on С++ 17... Тем временем нет необходимости в стирании типа. Вывод аргумента функции шаблона позволяет нам быть легким помощником:

template<class Mutex>
auto make_lock(Mutex& m)
{
    return std::unique_lock<Mutex>(m);
}

...

std::mutex m;
std::recursive_mutex m2;

auto lock = make_lock(m);
auto lock2 = make_lock(m2);