如何启用转换模板参数 T 以常量 T?
How to enable conversion template argument T to const T?
>假设我有一个以下类
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>;
};
演示
- 如何创建长度由常量参数指定的数组
- 通过常量引用传递参数的矩阵模板类
- 具有常量引用参数的函数模板专用化
- 使用自动推导的 lambda 参数作为常量表达式
- C++:常量引用参数
- 常量参数"real"常量吗?
- 常量参数存储在哪里 (C++)?
- 常量函数,当其参数是对文字类型的引用时
- 区分接受常量参数的函数引用/指针和与函数参数同名的非常量参数
- 必须非常量别名参数及其默认参数常量
- 字符串参数常量字符* 和常量 wchar_t*
- 可选参数常量引用重新分配
- 推导模板化类参数的模板参数:常量问题
- 从函数参数常量字符串 (&) 设置值
- 为什么我必须声明这些引用参数常量或按值传递
- C++使用一个参数常量重载
- 如果要执行const_cast,为什么要制作参数常量?
- 模板非类型参数常量限制筛选器库
- 标记方法指针/引用参数常量真的会显著影响性能吗
- 当函数参数常量引用 T 时,为什么 T 的模板参数推导'skips'数组元素的恒定性?