如何启用转换模板参数 T 到 const T?
How to enable conversion template argument T to const T?
假设我有一个关注者class
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)};
}
};
有更好的 "proper" 方法吗?
1. 通常,除了显式定义转换运算符之外,允许将 SomeType 转换为 SomeType 的正确方法是什么? (不仅在我的例子中)。
2. 如果需要定义转换运算符,reinterpret_cast 是正确的做法吗?或者有"cleaner"种方法?
编辑:答案和评论非常有帮助。我决定现在提供更多背景信息。我的问题不在于实现 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; }.
最初的解决方案是编写两个包装器 classes,一个带有 T& operator*() ,另一个不带。或者使用继承,迭代器从 const_iterator 派生(这可能是一个很好的解决方案并且有一个优势 - 我们不需要为迭代器重写比较运算符并且可以将迭代器与 const_iterator 进行比较 - 这最常见有道理 - 因为我们检查它们都指向同一个节点)。
但是,我很好奇如何在不继承或两次键入相同代码的情况下编写此代码。基本上,我认为需要生成一些条件模板——使用方法 T& operator*() { return node->value; } 只为迭代器而不是 const_iterator 生成。正确的做法是什么?如果const_iterator把Node*当成Node*,差不多就解决了我的问题。
也许你想要另一个 class,像这样:
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>;
};
Is there a better, "proper" way to do it?
必须存在,因为您的解决方案既有奇怪的行为,也按照 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>;
假设我有一个关注者class
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)};
}
};
有更好的 "proper" 方法吗? 1. 通常,除了显式定义转换运算符之外,允许将 SomeType 转换为 SomeType 的正确方法是什么? (不仅在我的例子中)。 2. 如果需要定义转换运算符,reinterpret_cast 是正确的做法吗?或者有"cleaner"种方法?
编辑:答案和评论非常有帮助。我决定现在提供更多背景信息。我的问题不在于实现 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; }.
最初的解决方案是编写两个包装器 classes,一个带有 T& operator*() ,另一个不带。或者使用继承,迭代器从 const_iterator 派生(这可能是一个很好的解决方案并且有一个优势 - 我们不需要为迭代器重写比较运算符并且可以将迭代器与 const_iterator 进行比较 - 这最常见有道理 - 因为我们检查它们都指向同一个节点)。
但是,我很好奇如何在不继承或两次键入相同代码的情况下编写此代码。基本上,我认为需要生成一些条件模板——使用方法 T& operator*() { return node->value; } 只为迭代器而不是 const_iterator 生成。正确的做法是什么?如果const_iterator把Node*当成Node*,差不多就解决了我的问题。
也许你想要另一个 class,像这样:
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>;
};
Is there a better, "proper" way to do it?
必须存在,因为您的解决方案既有奇怪的行为,也按照 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>;