如何启用转换模板参数 T 以常量 T?

How to enable conversion template argument T to const T?

本文关键字:参数 常量 转换 何启用 启用      更新时间:2023-10-16

>假设我有一个以下类

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正确的方法是什么?还是有"更干净"的方法?

编辑:答案和评论非常有帮助。我决定现在提供更多的背景信息。我的问题不在于实现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*(( {返回node->value; }。

最初的解决方案是编写两个包装类,一个带有T&operator*((,另一个没有。或者使用继承,迭代器派生自const_iterator(这可能是一个很好的解决方案并且具有优势 - 我们不需要为迭代器重写比较运算符,并且可以将迭代器与const_iterator进行比较 - 这通常是有意义的 - 因为我们检查它们都指向同一节点(。

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

有没有更好的、"适当"的方法呢?

必须有,因为您的解决方案既有奇怪的行为,又是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

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


你能做什么?

在评论中,您提到要实现一个 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->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>;

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

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

演示

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

演示