为什么 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)

有什么好处?我只看到问题在于:

编辑: 我已经接受了一个答案。既然我已经考虑了语言需要知道什么以及何时需要知道它,我认为它确实有意义。

你说的第二个:

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" 比您想象的要深得多。您正在查看 staticdynamic 类型系统之间的区别。我建议阅读 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++ 为这种情况提供了其他工具。
    例如,虽然你不能有一个包含 intstd::string 混合的 std::vector,但你完全可以有一个 std::vector<Widget*>,其中 IntWidget 包含一个intStringWidget 包含一个 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";
}