std :: reference_wrapper развернуть обертку - PullRequest
4 голосов
/ 30 января 2020

Введение.

В C ++ мы не можем создавать контейнеры ссылок:

std::vector<int&> vri;
In instantiation of ‘class __gnu_cxx::new_allocator<int&>’:
required from ‘class std::allocator<int&>’
required from ‘struct std::_Vector_base<int&, std::allocator<int&> >’
required from ‘class std::vector<int&>’
required from here
error: forming pointer to reference type ‘int&’
       typedef _Tp*       pointer;
                          ^~~~~~~

Внутренняя реализация требует создания указателя на содержащийся тип , что приводит к указателю на ссылку запрещенный тип.

К счастью, std::reference_wrapper существует:

int x{1}, y{2}, z{3};
std::vector<std::reference_wrapper<int>> vr{x, y, z};
for (auto &v : vr)
    ++v;
std::cout << x << ' ' << y << ' ' << z << '\n';

Код выше показывает 2 3 4.

Проблема.

Я работаю над утилитой фильтра C ++, например, фильтр where получает контейнер и возвращает std::reference_wrapper содержащимся объектам, которые соответствуют критериям:

template <typename container_t> auto range(const container_t &container)
{ return std::tuple{std::begin(container), std::end(container)}; };

template <typename container_t, typename predicate_t>
auto where(const container_t &container, predicate_t predicate)
{
    auto [b, e] = range(container);
    using type = std::remove_reference_t<decltype(*b)>;
    using reference = std::reference_wrapper<type>;

    std::vector<reference> result{};

    std::copy_if(b, e, std::back_inserter(result), predicate);

    return result;
}

Код ниже показывает 2 3 6 7:

int main()
{
    std::vector v{0, 1, 2, 3, 4, 5, 6, 7, 8, 9};

    for (auto &x : where(v, [](auto n){ return n & 0b10; }))
        std::cout << x << ' ';

    return 0;
}

Но у меня возникла проблема с фильтрами цепочек:

for (const auto &x :
    where(where(v, [](auto n){ return n & 0b10; }), [](auto n){ return n & 0b1; })) {
    std::cout << x << ' ';
}
no match for ‘operator<<’ (operand types are ‘std::ostream’ {aka ‘std::basic_ostream<char>’} and ‘const std::reference_wrapper<const std::reference_wrapper<const int> >’)
   std::cout << x << ' ';
   ~~~~~~~~~~^~~~

Внутренний where возвращает std::vector<std::refernce_wrapper<int>>, поэтому внешний будет использовать std::vector<std::refernce_wrapper<const std::refernce_wrapper<const int>>>.

Что я пробовал?.

Для решения проблемы я пытался создать шаблон, который разворачивает std::reference_wrapper<T>:

template <typename type_t>
struct unwrap
{
    using type = type_t;
};

template <typename type_t>
struct unwrap<std::reference_wrapper<type_t>>
{
    using type = type_t;
};

template <typename type_t>
using unwrap_t = typename unwrap<type_t>::type;

Пока все выглядит так, как будто он работает:

int main()
{
    using ri = std::reference_wrapper<int>;
    using rf = std::reference_wrapper<float>;
    using rri = std::reference_wrapper<ri>;
    using rrri = std::reference_wrapper<rri>;

    std::cout
        << typeid(int).name() << '\t' << typeid(unwrap_t<int>).name() << '\n'
        << typeid(float).name() << '\t' << typeid(unwrap_t<float>).name() << '\n'
        << typeid(ri).name() << '\t' << typeid(unwrap_t<ri>).name() << '\n'
        << typeid(rf).name() << '\t' << typeid(unwrap_t<rf>).name() << '\n'
        << typeid(rri).name() << '\t' << typeid(unwrap_t<rri>).name() << '\n'
        << typeid(rrri).name() << '\t' << typeid(unwrap_t<rrri>).name();

    return 0;
}

Получает проппер искаженные имена * 1 054 *:

  i   i
  f   f
  St17reference_wrapperIiE    i
  St17reference_wrapperIfE    f
  St17reference_wrapperIS_IiEE    St17reference_wrapperIiE
  St17reference_wrapperIS_IS_IiEEE    St17reference_wrapperIS_IiEE

Целые числа и числа с плавающей запятой (int, float) остаются неизменными, оболочки с целыми числами и числами с плавающей запятой разворачиваются, а вложенные оболочки разворачиваются на один уровень.

Но он не работает внутри where:

template <typename container_t, typename predicate_t>
auto where(const container_t &container, predicate_t predicate)
{
    auto [b, e] = range(container);
    using type = unwrap_t<std::remove_reference_t<decltype(*b)>>;
    //           ^^^^^^^^ <--- Unwraps iterator's inner type
    using reference = std::reference_wrapper<type>;

    std::vector<reference> result{};

    std::copy_if(b, e, std::back_inserter(result), predicate);

    // Debug
    std::cout
        << __PRETTY_FUNCTION__ << "\n"
        << '\t' << "decltype(*b) = " << typeid(decltype(*b)).name() << '\n'
        << '\t' << "unwrap *b = " << typeid(unwrap_t<decltype(*b)>).name() << '\n'
        << '\t' << "type = " << typeid(type).name() << '\n'
        << '\t' << "reference = " << typeid(reference).name() << '\n'
        << '\t' << "unwrap type = " << typeid(unwrap_t<type>).name() << '\n';

    return result;
}

int main()
{
    std::vector v{0, 1, 2, 3, 4, 5, 6, 7, 8, 9};

    for (const auto &x :
        where(where(v, [](auto n){ return n & 0b10; }), [](auto n){ return n & 0b1; })) {
        std::cout << &x << ' ';
    }

    return 0;
}

Отладка журналов в where показывает, что распаковщик работал при первом вызове (он ничего не сделал для типа), но не во втором вызове:

  auto where(const container_t&, predicate_t) [with container_t = std::vector<int, std::allocator<int> >; predicate_t = main()::<lambda(auto:1)>]
      decltype(*b) = i
      unwrap *b = i
      type = i
      reference = St17reference_wrapperIKiE
      unwrap type = i
  auto where(const container_t&, predicate_t) [with container_t = std::vector<std::reference_wrapper<const int>, std::allocator<std::reference_wrapper<const int> > >; predicate_t = main()::<lambda(auto:2)>]
      decltype(*b) = St17reference_wrapperIKiE
      unwrap *b = St17reference_wrapperIKiE
      type = St17reference_wrapperIKiE
      reference = St17reference_wrapperIKS_IKiEE
      unwrap type = St17reference_wrapperIKiE

При внутреннем вызове контейнер ввода равен std::vector<int>, поэтому внутренний тип итератора (decltype(*b)), развертывание (unwrap_t<decltype(*b)>) ), тип связи (type) и развернутый тип (unwrap_t<type>): int, только reference - std::reference_wrapper.

При внешнем вызове контейнер ввода равен std::vector<std::reference_wrapper<const int>> и все типы (кроме reference) равны std::reference_wrapper<const int>, как если бы распаковщик игнорировал тип ввода.

Вопрос.

Что я делаю неправильно в моем распаковщике? Я думаю, что проблема может быть связана с распространением const.

Код доступен на Попробуйте онлайн! .

1 Ответ

4 голосов
/ 30 января 2020

Думаю, проблема в том, что *b возвращает значение const (поскольку контейнер передается по ссылке const). Ваш unwrap работает только на неконстантных, энергонезависимых reference_wrapper. В этой задаче я бы go сделал следующее:

#include <functional>

namespace detail{
template <typename type_t, class  orig_t>
struct unwrap_impl
{
    using type = orig_t;
};

template <typename type_t, class V>
struct unwrap_impl<std::reference_wrapper<type_t>,V>
{
    using type = type_t;
};
}

template<class T>
struct unwrap {
  using type = typename detail::unwrap_impl<std::decay_t<T>, T>::type;
};

template <typename type_t>
using unwrap_t = typename unwrap<type_t>::type;

int main() {
    static_assert(std::is_same_v<const int&, unwrap_t<const int &>>);
        static_assert(std::is_same_v<const int&, unwrap_t<std::reference_wrapper<const int &>>>);
        static_assert(std::is_same_v<const int&, unwrap_t<const std::reference_wrapper<const int &>&>>);
}

Это должно вернуть исходный тип для всего, что не reference_wrapper, а внутренний тип для cv-квалифицированных reference_wrapper s и ссылки на них.


Объяснение : я назову оригинал unwrap из OP UNWRAP в следующем, чтобы отличаться от моей версии. Мы хотим вызывать спецификацию reference_wrapper UNWRAP всякий раз, когда std::decay_t<T> является std::reference_wrapper. Теперь это может быть просто выполнено, если мы всегда будем вызывать UNWRAP с std::decay_t<T> вместо T.

Проблема в том, что если T не является reference_wrapper, это удалит все квалификации, т.е. UNWRAP<std::decay_t<const int>> равно int, когда мы хотим, чтобы оно было const int.

Чтобы обойти это, мы определяем нас template<class type_t, class orig_t> struct unwrap_impl. Мы хотим всегда передавать этот распавшийся тип для первого аргумента и исходный тип (до распада) в качестве второго аргумента. Затем мы можем передать общий случай orig_t в качестве типа результата (как это сделано с помощью using type = orig_t).

Для спецификации мы определим template<class type_t, class V> struct unwrap_impl<std::reference_wrapper<type_t>, V>. Это будет применяться всякий раз, когда type_t является reference_wrapper, т.е. когда тип orignal является некоторой квалификацией reference_wrapper. Нас не волнует второй аргумент (который будет исходным типом), поэтому мы просто игнорируем его. Затем мы берем внутренний тип reference_wrapper как тип (using type = type_t;).

Затем мы вызываем unwrap_impl, определяя в основном template<class type_t> unwrap = detail::unwrap_impl<std::decay_t<type_t>, type_t>; (это псевдокод, но я думаю, что это делает его более ясным.

Некоторые примеры:

unwrap<int> -> unwrap_impl<int, int> -> int
unwrap<const int> -> unwrap_impl<int, const int> -> const int
unwrap<std::reference_wrapper<const int>> -> unwrap_impl<std::reference_wrapper<const int>, std::reference_wrapper<const int>> -> const int
unwrap<const std::reference_wrapper<const int>> -> unwrap_impl<const std::reference_wrapper<const int>, const std::reference_wrapper<const int>> -> const int

(Снова псевдокод, но я надеюсь, что он очищен)

Редактировать: исправлены некоторые ошибки.

Edit2: Кажется, работает : ссылка

...