【问题标题】:Why are C++ tuples so weird?为什么 C++ 元组如此奇怪?
【发布时间】:2019-07-08 15:09:17
【问题描述】:

我通常在将不同类型的值分组在一起时创建自定义structs。这通常很好,我个人发现命名成员访问更容易阅读,但我想创建一个更通用的 API。在其他语言中广泛使用元组后,我想返回 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 语句)或者不可能。

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

【问题讨论】:

  • "按运行时索引"。也就是说,类型应该在编译时就知道了,所以你不能使用运行时的值作为索引。
  • 不是为什么它必须是模板函数的原因,但这是它没有成为成员函数的原因:stackoverflow.com/questions/3313479/…
  • 用你的 get 如何根据计算的索引有一个专用的返回类型?在集合中,您有 一个 类型,而不是元组
  • @Jarod42 这不是一个主要缺点吗?如果对它们的使用施加了如此严格的限制,为什么还要使用该机制来实现它们?
  • “使用这样的类型参数看起来很奇怪。” - 它不是类型参数。模板参数可以是编译时整数值而不是类型(并且用于get&lt;N&gt;)。

标签: c++ c++11 tuples c++-standard-library


【解决方案1】:

你说的第二个:

这使得通过运行时生成的索引进行索引变得困难(例如,对于一个小的有限范围索引,我已经看到代码对每种可能性都使用 switch 语句)或者如果范围太大则不可能。

C++ 是一种 静态类型语言,必须决定所涉及的类型compile-time

所以函数为

template <typename ... Ts>
auto foo (std::tuple<Ts...> const & t, std::size_t index)
 { return get(t, index); }

不可接受,因为返回的类型取决于运行时值index

采用的解决方案:将索引值作为编译时值传递,作为模板参数传递。

如您所知,我想,std::array 的情况完全不同:您有一个接收运行时索引值的get()(方法at(),或operator[]):在std::array 中,值类型不依赖于索引。

【讨论】:

  • 除非我弄错了,否则这是关于静态类型,而不是强类型。 Python 也是强类型(但不是静态类型)。这就是为什么您可以在运行时在 Python 中获得类型错误,而这些错误在(等效)C++ 中的编译时会被捕获,也是为什么在 Python 中元组访问受到较少限制的原因。见en.wikipedia.org/wiki/Type_system#Type_checking
  • 如此强类型的语言具有一些常见的基本类型(如 Object)可以避免返回它,但因为 C++ 在调用 get 时不需要知道确切的类型?
  • @DuncanACoulter 如果你有一个通用的多态基类型,你可以使用类似std::vector&lt;std::unique_ptr&lt;Base&gt;&gt; 的东西来得到你想要的。但这只有在您的容器可以存储的每种类型都是 BaseBase 派生类型时才有效。如果甚至有一个其他类型,您必须回退到std::vector&lt;std::variant&lt;[supported types]&gt;&gt;std::vector&lt;std::any&gt;。编辑:std::variant 限制了您如何构建代码,并可能有助于说明为什么 std::get 是这样的。
  • @FrançoisAndrieux 好吧,在我看来就是这样。谢谢你。
  • 我已经接受了答案。现在我已经考虑了语言需要知道什么以及什么时候需要知道,我认为它确实有意义。
【解决方案2】:

std::get&lt;N&gt; 中要求模板参数的“工程决策”比您想象的要深得多。您正在查看 staticdynamic 类型系统之间的区别。我推荐阅读https://en.wikipedia.org/wiki/Type_system,但这里有几个要点:

  • 在静态类型中,变量/表达式的类型必须在编译时知道。在这种情况下,std::tuple&lt;int, std::string&gt;get(int) 方法不能存在,因为在编译时无法知道 get 的参数。另一方面,由于模板参数必须在编译时已知,因此在这种情况下使用它们非常有意义。

  • C++ 也具有多态类形式的动态类型。这些利用运行时类型信息 (RTTI),会带来性能开销std::tuple 的正常用例不需要动态类型,因此它不允许这样做,但 C++ 为这种情况提供了其他工具。
    例如,虽然您不能拥有一个包含intstd::string 混合的std::vector,但您完全可以拥有一个std::vector&lt;Widget*&gt;,其中IntWidget 包含一个int,而StringWidget 包含一个@ 987654334@,只要两者都派生自 Widget。假设,说,

    struct Widget {
       virtual ~Widget();
       virtual void print();
    };
    

    您可以在向量的每个元素上调用print,而无需知道其确切(动态)类型。

【讨论】:

  • +1 用于启动 RTTI。我会尽量不被你对类型系统维基百科页面的推荐所冒犯,但我又一次很密集,所以我想这是公平的。
【解决方案3】:
  • 看起来很奇怪

这是一个弱论点。外观是一个主观问题。

函数参数列表根本不是编译时需要的值的选项。

  • 这使得通过运行时生成的索引进行索引变得困难

无论如何,运行时生成索引都很困难,因为 C++ 是一种静态类型语言,没有运行时反射(甚至编译时反射)。考虑以下程序:

std::tuple<std::vector<C>, int> tuple;
int index = get_at_runtime();
WHATTYPEISTHIS var = get(tuple, index);

get(tuple, index) 的返回类型应该是什么?你应该初始化什么类型的变量?它不能返回一个向量,因为index 可能是 1,它不能返回一个整数,因为index 可能是 0。所有变量的类型在 C++ 编译时是已知的。

当然,C++17 引入了std::variant,在这种情况下这是一个潜在的选择。元组是在 C++11 中引入的,这不是一个选项。

如果您需要元组的运行时索引,您可以编写自己的get 函数模板,该模板采用元组和运行时索引并返回std::variant。但是使用变体并不像直接使用类型那么简单。这就是将运行时类型引入静态类型语言的成本。

【讨论】:

  • 我知道陌生感是主观的,是一个弱的论据。与其他容器相比,它更像是一种评论。但是,我看到像 Scala 中的其他静态类型元组也可以通过“奇怪的”编译类型可见成员(如 ._1 ._2 等)访问,因此在反射上确实有意义。 +1 提到 std::variant 虽然我不会使用它。
【解决方案4】:

请注意,在 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";
}

【讨论】:

  • 我要偷这个。哈哈!很好的解决方案。 =)
  • 我现在经常使用结构化绑定。 +1 是一个不错的选择。
猜你喜欢
  • 1970-01-01
  • 1970-01-01
  • 2018-07-21
  • 1970-01-01
  • 2021-11-01
  • 1970-01-01
  • 1970-01-01
  • 2013-07-14
  • 2017-03-03
相关资源
最近更新 更多