为什么 C++ 元组如此奇怪?
Why are C++ tuples so weird?
我通常会在将不同类型的值组合在一起时创建自定义 structs
。这通常很好,我个人觉得命名成员访问更容易阅读,但我想创建一个更通用的目的 API。在其他语言中广泛使用元组后,我想 return 类型 std::tuple
的值,但发现它们在 C++ 中的使用比在其他语言中更难看。
为了使元素访问使用如下 get
的整数值模板参数,进行了哪些工程决策?
#include <iostream>
#include <tuple>
using namespace std;
int main()
{
auto t = make_tuple(1.0, "Two", 3);
cout << "(" << get<0>(t) << ", "
<< get<1>(t) << ", "
<< get<2>(t) << ")\n";
}
而不是像下面这样简单的东西?
t.get(0)
或
get(t,0)
有什么好处?我只看到问题在于:
- 这样使用模板参数看起来很奇怪。我知道模板语言是图灵完备的,但仍然...
- 这使得通过运行时生成的索引进行索引变得困难(例如,对于一个小的有限范围索引,我已经看到针对每种可能性使用 switch 语句的代码)或者如果范围太大则不可能。
编辑: 我已经接受了一个答案。既然我已经考虑了语言需要知道什么以及何时需要知道它,我认为它确实有意义。
你说的第二个:
It makes indexing by runtime generated indices difficult (for example for a small finite ranged index I've seen code using switch statements for each possibility) or impossible if the range is too large.
C++ 是一种强 静态类型语言,必须决定涉及的类型compile-time
所以函数为
template <typename ... Ts>
auto foo (std::tuple<Ts...> const & t, std::size_t index)
{ return get(t, index); }
不可接受,因为返回的类型取决于 run-time 值 index
。
采用的解决方案:将索引值作为编译时值传递,作为模板参数传递。
如你所知,我想,std::array
的情况完全不同:你有一个 get()
(方法 at()
,或者 operator[]
) 接收 run-time 索引值:在 std::array
中,值类型不依赖于索引。
std::get<N>
中需要模板参数的 "engineering decisions" 比您想象的要深得多。您正在查看 static 和 dynamic 类型系统之间的区别。我建议阅读 https://en.wikipedia.org/wiki/Type_system,但这里有几个要点:
在静态类型中,variable/expression 的类型必须 在 compile-time 处已知。 std::tuple<int, std::string>
的 get(int)
方法在这种情况下不存在,因为 get
的参数无法在 compile-time 处获知。另一方面,由于模板参数必须在 compile-time 处已知,因此在此上下文中使用它们非常有意义。
C++ 也有多态形式的动态类型类。这些利用 run-time 类型信息 (RTTI), 会带来性能开销 。 std::tuple
的正常用例不需要动态类型,因此它不允许这样做,但 C++ 为这种情况提供了其他工具。
例如,虽然你不能有一个包含 int
和 std::string
混合的 std::vector
,但你完全可以有一个 std::vector<Widget*>
,其中 IntWidget
包含一个int
和 StringWidget
包含一个 std::string
,只要它们都派生自 Widget
。假设,
struct Widget {
virtual ~Widget();
virtual void print();
};
您可以在不知道其确切(动态)类型的情况下对向量的每个元素调用 print
。
- It looks very strange
这是一个弱论点。长相是主观的。
函数参数列表根本不是编译时所需值的选项。
- It makes indexing by runtime generated indices difficult
无论如何,运行时生成索引都很困难,因为 C++ 是一种静态类型语言,没有运行时反射(甚至编译时反射)。考虑以下程序:
std::tuple<std::vector<C>, int> tuple;
int index = get_at_runtime();
WHATTYPEISTHIS var = get(tuple, index);
get(tuple, index)
的 return 类型应该是什么?你应该初始化什么类型的变量?它不能 return 一个向量,因为 index
可能是 1,它不能 return 一个整数,因为 index
可能是 0。所有变量的类型在编译时都是已知的C++ 中的时间。
当然,C++17 引入了 std::variant
,在这种情况下这是一个潜在的选择。元组是在 C++11 中引入的,这不是一个选项。
如果您需要元组的运行时索引,您可以编写自己的 get
函数模板,该模板采用元组和运行时索引以及 return 和 std::variant
。但是使用变体并不像直接使用类型那么简单。这就是将运行时类型引入静态类型语言的代价。
请注意,在 C++17 中,您可以使用 structured binding 使这一点更加明显:
#include <iostream>
#include <tuple>
using namespace std;
int main()
{
auto t = make_tuple(1.0, "Two", 3);
const auto& [one, two, three] = t;
cout << "(" << one << ", "
<< two << ", "
<< three << ")\n";
}
我通常会在将不同类型的值组合在一起时创建自定义 structs
。这通常很好,我个人觉得命名成员访问更容易阅读,但我想创建一个更通用的目的 API。在其他语言中广泛使用元组后,我想 return 类型 std::tuple
的值,但发现它们在 C++ 中的使用比在其他语言中更难看。
为了使元素访问使用如下 get
的整数值模板参数,进行了哪些工程决策?
#include <iostream>
#include <tuple>
using namespace std;
int main()
{
auto t = make_tuple(1.0, "Two", 3);
cout << "(" << get<0>(t) << ", "
<< get<1>(t) << ", "
<< get<2>(t) << ")\n";
}
而不是像下面这样简单的东西?
t.get(0)
或
get(t,0)
有什么好处?我只看到问题在于:
- 这样使用模板参数看起来很奇怪。我知道模板语言是图灵完备的,但仍然...
- 这使得通过运行时生成的索引进行索引变得困难(例如,对于一个小的有限范围索引,我已经看到针对每种可能性使用 switch 语句的代码)或者如果范围太大则不可能。
编辑: 我已经接受了一个答案。既然我已经考虑了语言需要知道什么以及何时需要知道它,我认为它确实有意义。
你说的第二个:
It makes indexing by runtime generated indices difficult (for example for a small finite ranged index I've seen code using switch statements for each possibility) or impossible if the range is too large.
C++ 是一种强 静态类型语言,必须决定涉及的类型compile-time
所以函数为
template <typename ... Ts>
auto foo (std::tuple<Ts...> const & t, std::size_t index)
{ return get(t, index); }
不可接受,因为返回的类型取决于 run-time 值 index
。
采用的解决方案:将索引值作为编译时值传递,作为模板参数传递。
如你所知,我想,std::array
的情况完全不同:你有一个 get()
(方法 at()
,或者 operator[]
) 接收 run-time 索引值:在 std::array
中,值类型不依赖于索引。
std::get<N>
中需要模板参数的 "engineering decisions" 比您想象的要深得多。您正在查看 static 和 dynamic 类型系统之间的区别。我建议阅读 https://en.wikipedia.org/wiki/Type_system,但这里有几个要点:
在静态类型中,variable/expression 的类型必须 在 compile-time 处已知。
std::tuple<int, std::string>
的get(int)
方法在这种情况下不存在,因为get
的参数无法在 compile-time 处获知。另一方面,由于模板参数必须在 compile-time 处已知,因此在此上下文中使用它们非常有意义。C++ 也有多态形式的动态类型类。这些利用 run-time 类型信息 (RTTI), 会带来性能开销 。
std::tuple
的正常用例不需要动态类型,因此它不允许这样做,但 C++ 为这种情况提供了其他工具。
例如,虽然你不能有一个包含int
和std::string
混合的std::vector
,但你完全可以有一个std::vector<Widget*>
,其中IntWidget
包含一个int
和StringWidget
包含一个std::string
,只要它们都派生自Widget
。假设,struct Widget { virtual ~Widget(); virtual void print(); };
您可以在不知道其确切(动态)类型的情况下对向量的每个元素调用
print
。
- It looks very strange
这是一个弱论点。长相是主观的。
函数参数列表根本不是编译时所需值的选项。
- It makes indexing by runtime generated indices difficult
无论如何,运行时生成索引都很困难,因为 C++ 是一种静态类型语言,没有运行时反射(甚至编译时反射)。考虑以下程序:
std::tuple<std::vector<C>, int> tuple;
int index = get_at_runtime();
WHATTYPEISTHIS var = get(tuple, index);
get(tuple, index)
的 return 类型应该是什么?你应该初始化什么类型的变量?它不能 return 一个向量,因为 index
可能是 1,它不能 return 一个整数,因为 index
可能是 0。所有变量的类型在编译时都是已知的C++ 中的时间。
当然,C++17 引入了 std::variant
,在这种情况下这是一个潜在的选择。元组是在 C++11 中引入的,这不是一个选项。
如果您需要元组的运行时索引,您可以编写自己的 get
函数模板,该模板采用元组和运行时索引以及 return 和 std::variant
。但是使用变体并不像直接使用类型那么简单。这就是将运行时类型引入静态类型语言的代价。
请注意,在 C++17 中,您可以使用 structured binding 使这一点更加明显:
#include <iostream>
#include <tuple>
using namespace std;
int main()
{
auto t = make_tuple(1.0, "Two", 3);
const auto& [one, two, three] = t;
cout << "(" << one << ", "
<< two << ", "
<< three << ")\n";
}