Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

How to enable conversion template argument T to const T?

Suppose that I have a following class

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

Often one needs to write code similar to this (let's assume that Sometype is std::string for now, although I don't think that it matters).

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

One way to work around is to define explicit conversion operator:

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)};
    }
};

Is there a better, "proper" way to do it? 1. In general, what is the proper way to allow conversion of SomeType to SomeType except explicitly defining conversion operator? (Not in my example only). 2. If defining conversion operator is necessary, is reinterpret_cast is the proper way to do it? Or there are "cleaner" ways?

EDIT: Answers and comments were very helpful. I decided to provide more context right now. My problem is not with implementing const_iterator itself (I think that I know how to do it), but how to use same template for iterator and const_iterator. Here is what I mean

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;
};

Implementing const_iterator is essentially the same, except that T& operator*() { return node->value; }.

The initial solution is just to write two wrapper classes, one with T& operator*() and the other one without. Or use inheritance, with iterator deriving from const_iterator (which might be a good solution and has an advantage - we don't need to rewrite comparison operators for iterator and can compare iterator with const_iterator - which most often makes sense - as we check that they both point at same node).

However, I am curious how to write this without inheritance or typing same code twice. Basically, I think that some conditional template generation is needed - to have the method T& operator*() { return node->value; } generated only for iterator and not const_iterator. What is the proper way to do it? If const_iterator treated the Node* as Node*, it almost solves my problem.

like image 672
Raziel Magius Avatar asked Sep 21 '25 05:09

Raziel Magius


2 Answers

Is there a better, "proper" way to do it?

There must be since your solution both has a weird behavior and is also invalid as specified by the C++ standard.

There's a rule called strict aliasing which dictate what kind of pointer type can alias another type. For example, both char* and std::byte* can alias any type, so this code is valid:

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
}

But, you cannot make any type alias another:

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

In the C++ type system, each instantiation of a template type are different, unrelated types. So in your example, Node<int> is a completely, unrelated, different type than Node<int const>.

I also said that your code has a very strange behavior?

Consider this code:

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;

This will output the following:

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

As you can see, you copy only one data, but not the rest of the nodes.


What can you do?

In the comments you mentionned that you wanted to implement a const iterator. That can be done without changing your data structures:

// 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;
};

Since you contain a pointer to constant node, you cannot mutate the value inside of the node. The expression node->value yield a T const&.

Since the nodes are there only to implement List, I will assume they are abstracted away completely and never exposed to the users of the list.

If so, then you never have to convert a node, and operate on pointer to constant inside the implementation of the list and its iterators.

To reuse the same iterator, I would do something like this:

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>>;

Then simply make your iterator type parameterized by the boolean:

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>;
like image 197
Guillaume Racicot Avatar answered Sep 22 '25 18:09

Guillaume Racicot


Maybe you want another class altogether, like this:

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

If however you are talking about an iterator or a fancy pointer (as you mention in the comments), it's quite easy to do with an additional template parameter and some 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

like image 33
Nelfeal Avatar answered Sep 22 '25 18:09

Nelfeal



Donate For Us

If you love us? You can donate to us via Paypal or buy me a coffee so we can maintain and grow! Thank you!