Как включить аргумент шаблона преобразования T в const T? - PullRequest
3 голосов
/ 26 мая 2020

Предположим, что у меня есть следующий класс

template <typename T>
struct Node { T value; Node* next; };

Часто нужно писать код, похожий на этот (предположим, что Sometype - это std :: string на данный момент, хотя я не думаю, что это имеет значение ).

Node<SomeType> node = Node{ someValue, someNodePtr };
...
Node <const SomeType> constNode = node; // compile error

Один из способов решения проблемы - определить явный оператор преобразования:

template <typename T>
struct Node
{
    T value;
    Node* next;
    operator Node<const T>() const { 
        return Node<const T>{value, reinterpret_cast<Node<const T>* >(next)};
    }
};

Есть ли лучший, «правильный» способ сделать это? 1. В целом, как правильно разрешить преобразование SomeType в SomeType, кроме явного определения оператора преобразования? (Не только в моем примере). 2. Если определение оператора преобразования необходимо, является ли reinterpret_cast правильным способом сделать это? Или есть более «чистые» способы?

EDIT: ответы и комментарии были очень полезны. Я решил предоставить больше контекста прямо сейчас. Моя проблема заключается не в реализации самого const_iterator (я думаю, что я знаю, как это сделать), а в том, как использовать один и тот же шаблон для итератора и const_iterator. Вот что я имею в виду

template <typename T>
struct iterator
{
    iterator(Node<T>* _node) : node{ _node } {}
    T& operator*() { return node->value; } // for iterator only
    const T& operator*() const { return node->value; } // we need both for iterator 
                                                       // for const iterator to be usable

    iterator& operator++() { node = node->next; return *this; }
    iterator operator++(int) { auto result = iterator{ node }; node = node->next; return result; }

    bool operator==(const iterator& other) { return node == other.node; }
    bool operator!=(const iterator& other) { return Node != other.node; }

private:
    Node<T>* node;
};

Реализация const_iterator практически такая же, за исключением того, что T & operator * () {return node-> value; }.

Первоначальное решение - просто написать два класса-оболочки, один с оператором T & * (), а другой - без него. Или используйте наследование с итератором, производным от const_iterator (что может быть хорошим решением и имеет преимущество - нам не нужно переписывать операторы сравнения для итератора и мы можем сравнивать итератор с const_iterator, что чаще всего имеет смысл, поскольку мы проверяем, что они оба указывают на один и тот же узел).

Однако мне любопытно, как написать это без наследования или ввода одного и того же кода дважды. В принципе, я думаю, что необходима некоторая условная генерация шаблона - чтобы был метод T & operator * () {return node-> value; } генерируется только для итератора, а не для const_iterator. Как правильно это сделать? Если const_iterator рассматривает Узел * как Узел *, это почти решает мою проблему.

Ответы [ 2 ]

2 голосов
/ 26 мая 2020

Есть ли лучший, «правильный» способ сделать это?

Должен быть, так как ваше решение имеет странное поведение и недействительно в соответствии со стандартом C ++.

Существует правило, называемое строгим псевдонимом, которое определяет, какой тип указателя может использовать псевдоним другого типа. Например, и char*, и std::byte* могут быть псевдонимами любого типа, поэтому этот код действителен:

struct A {
    // ... whatever
};

int main() {
    A a{};
    std::string b;

    char* aptr = static_cast<void*>(&a);          // roughtly equivalent to reinterpret
    std::byte* bptr = reintepret_cast<std::byte*>(&b); // static cast to void works too
}

Но вы не можете сделать псевдоним любого типа другим:

double a;
int* b = reinterpret_cast<int*>(&a); // NOT ALLOWED, undefined behavior

В системе типов C ++ каждая реализация типа шаблона - это разные, не связанные между собой типы. Итак, в вашем примере Node<int> - это совершенно несвязанный тип, отличный от Node<int const>.

Я также сказал, что ваш код ведет себя очень странно?

Рассмотрим этот код:

struct A {
    int n;
    A(int _n) : n(_n) { std::cout << "construct " << n << std::endl; }
    A(A const&) { std::cout << "copy " << n << std::endl; }
    ~A() { std::cout << "destruct " << n << std::endl; }
};

Node<A> node1{A{1}};
Node<A> node2{A{2}};
Node<A> node3{A{3}};

node1.next = &node2;
node2.next = &node3;

Node<A const> node_const = node1;

Это выведет следующее:

construct 1
construct 2
construct 3
copy 1
destruct 1
destruct 3
destruct 2
destruct 1

Как видите, вы копируете только одни данные, но не остальные узлы.


Что вы умеете?

В комментариях вы упомянули, что хотите реализовать константный итератор. Это можно сделать без изменения ваших структур данных:

// inside list's scope
struct list_const_iterator {

    auto operator*() -> T const& {
        return node->value;
    }

    auto operator++() -> node_const_iterator& {
        node = node->next;
        return *this;
    }

private:
    Node const* node;
};

Поскольку вы содержат указатель на постоянный узел, вы не можете изменять value внутри узла. Выражение node->value дает T const&.

Поскольку узлы предназначены только для реализации List, я предполагаю, что они полностью абстрагированы и никогда не открываются пользователям списка.

Если это так, то вам никогда не придется преобразовывать узел и работать с указателем на константу внутри реализации списка и его итераторов.

Чтобы повторно использовать тот же итератор, я бы сделал что-то вроде этого:

template<typename T>
struct iterator_base {
    using reference = T&;
    using node_pointer = Node<T>*;
};

template<typename T>
struct const_iterator_base {
    using reference = T const&;
    using node_pointer = Node<T> const*;
};

template<typename T, bool is_const>
using select_iterator_base = std::conditional_t<is_const, const_iterator_base<T>, iterator_base<T>>;

Затем просто сделайте свой тип итератора параметризованным логическим значением:

template<bool is_const>
struct list_basic_iterator : select_iterator_base<is_const> {

    auto operator*() -> typename select_iterator_base<is_const>::reference {
        return node->value;
    }

    auto operator++() -> list_basic_iterator& {
        node = node->next;
        return *this;
    }

private:
    typename select_iterator_base<is_const>::node_ptr node;
};

using iterator = list_basic_iterator<false>;
using const_iterator = list_basic_iterator<true>;
1 голос
/ 26 мая 2020

Может быть, вам вообще нужен другой класс, например:

template<typename T>
struct NodeView
{
    T const& value; // Reference or not (if you can make a copy)
    Node<T>* next;

    NodeView(Node<T> const& node) :
    value(node.value), next(node.next) {
    }
};

Demo

Однако если вы говорите об итераторе или необычном указателе (как вы упоминание в комментариях), это довольно просто сделать с дополнительным параметром шаблона и некоторыми std::conditional:

template<typename T, bool C = false>
class Iterator {
public:
    using Pointer = std::conditional_t<C, T const*, T*>;
    using Reference = std::conditional_t<C, T const&, T&>;

    Iterator(Pointer element) :
    element(element) {
    }
    Iterator(Iterator<T, false> const& other) :
    element(other.element) {
    }

    auto operator*() -> Reference {
        return *element;
    }

private:
    Pointer element;

    friend Iterator<T, !C>;
};

Demo

...