Search code examples
c++template-argument-deduction

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.


Solution

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