【问题标题】:How to enable conversion template argument T to const T?如何启用将模板参数 T 转换为 const T?
【发布时间】:2020-05-26 14:26:34
【问题描述】:

假设我有以下课程

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 是正确的方法吗?还是有“更清洁”的方式?

编辑:答案和 cmets 非常有帮助。我决定现在提供更多背景信息。我的问题不在于实现 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& operator*(),另一个没有。或者使用继承,迭代器从 const_iterator 派生(这可能是一个很好的解决方案并且有一个优势——我们不需要为迭代器重写比较运算符,并且可以将迭代器与 const_iterator 进行比较——这通常是有意义的——因为我们检查它们都指向同一个节点)。

但是,我很好奇如何在不继承或两次键入相同代码的情况下编写此代码。基本上,我认为需要一些条件模板生成 - 让方法 T& operator*() { return node->value; } 只为迭代器而不是 const_iterator 生成。正确的方法是什么?如果 const_iterator 将 Node* 视为 Node*,它几乎解决了我的问题。

【问题讨论】:

  • 您的重新解释转换是未定义的行为,因为它破坏了严格的别名。
  • 怎么会出现这种情况?它通常不应该发生。 Node&lt;SomeType&gt;Node &lt;const SomeType&gt; 是两个不相关的类型,它们不妨命名为PT
  • 为什么需要Node&lt;const T&gt; 而不是const Node&lt;T&gt;?你有一个用例吗?
  • @Nelfeal - 例如实现 const_iterator。
  • 或者,如果我编写了一个智能指针版本,并且我希望允许将 SmartPointer 转换为 SmartPointer,因为 T* 到 const T* 是有效的转换。

标签: c++ template-argument-deduction


【解决方案1】:

有没有更好、“正确”的方法?

必须有,因为您的解决方案既具有怪异的行为,而且按照 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&lt;int&gt; 是与 Node&lt;int const&gt; 完全不相关的不同类型。

我还说你的代码有一个很奇怪的行为?

考虑这段代码:

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

如您所见,您只复制一个数据,而不复制其余节点。


你能做什么?

在您提到的 cmets 中,您想要实现一个 const 迭代器。这可以在不更改数据结构的情况下完成:

// 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-&gt;value 产生一个T const&amp;

由于这些节点只是为了实现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>;

【讨论】:

  • 我添加了一些上下文。我的问题本质上是迭代器和 const_iterator 之间的代码重用,而不是如何实现它们......
【解决方案2】:

也许你完全想要另一个课程,像这样:

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

但是,如果您谈论的是迭代器或花哨的指针(正如您在 cmets 中提到的),那么使用额外的模板参数和一些 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

【讨论】:

  • 我认为应该使用 Pointer = std::conditional_t 并使用 Reference = std::conditional_t。我认为 T& const 不是有效类型。
  • @RazielMagius const T*T const* 是相同的。 T* const 是一个常量指针。 T&amp; const 确实无效,因为引用总是不变的。
  • @RazielMagius 你很困惑。查看错误hereconst 限定了它左边的东西,除非它的左边没有任何东西,因为它在类型的开头,在这种情况下它限定了它右边的东西。
猜你喜欢
  • 1970-01-01
  • 2017-10-07
  • 1970-01-01
  • 2016-02-13
  • 1970-01-01
  • 1970-01-01
  • 2011-01-18
  • 2020-10-17
  • 2013-10-20
相关资源
最近更新 更多