MediaPipe框架- 用C++语言构建图

Building Graphs in C++

用C++语言构建图

C++ graph builder is a powerful tool for:

C++图生成器是一个强大的工具,用于:

  • Building complex graphs
  • 构建复杂的图
  • Parametrizing graphs (e.g. setting a delegate on InferenceCalculator, enabling/disabling parts of the graph)
  • 对图进行参数化(例如,在推理计算器上设置代理,启用/禁用图的某些部分)
  • Deduplicating graphs (e.g. instead of CPU and GPU dedicated graphs in pbtxt you can have a single code that constructs required graphs, sharing as much as possible)
  • 重复数据消除图(例如,pbtxt中的CPU和GPU专用图形可以由一个代码构建所需的图,并尽可能多地共享)
  • Supporting optional graph inputs/outputs
  • 支持可选的图输入/输出
  • Customizing graphs per platform
  • 按平台自定义图

Basic Usage

基本用法

Let's see how C++ graph builder can be used for a simple graph:

让我们看看C++图生成器如何用于一个简单的图:

# Graph inputs.
input_stream: "input_tensors"
input_side_packet: "model"

# Graph outputs.
output_stream: "output_tensors"

node {
  calculator: "InferenceCalculator"
  input_stream: "TENSORS:input_tensors"
  input_side_packet: "MODEL:model"
  output_stream: "TENSORS:output_tensors"
  options: {
    [drishti.InferenceCalculatorOptions.ext] {
      # Requesting GPU delegate.
      delegate { gpu {} }
    }
  }
}

Function to build the above CalculatorGraphConfig may look like:

用于构建上述CalculatorGraphConfig的函数可能如下所示:

CalculatorGraphConfig BuildGraph() {
  Graph graph;

  // Graph inputs.
  Stream<std::vector<Tensor>> input_tensors =
      graph.In(0).SetName("input_tensors").Cast<std::vector<Tensor>>();
  SidePacket<TfLiteModelPtr> model =
      graph.SideIn(0).SetName("model").Cast<TfLiteModelPtr>();

  auto& inference_node = graph.AddNode("InferenceCalculator");
  auto& inference_opts =
      inference_node.GetOptions<InferenceCalculatorOptions>();
  // Requesting GPU delegate.
  inference_opts.mutable_delegate()->mutable_gpu();
  input_tensors.ConnectTo(inference_node.In("TENSORS"));
  model.ConnectTo(inference_node.SideIn("MODEL"));
  Stream<std::vector<Tensor>> output_tensors =
      inference_node.Out("TENSORS").Cast<std::vector<Tensor>>();

  // Graph outputs.
  output_tensors.SetName("output_tensors").ConnectTo(graph.Out(0));

  // Get `CalculatorGraphConfig` to pass it into `CalculatorGraph`
  return graph.GetConfig();
}

Short summary:

简短总结:

  • Use Graph::In/SideIn to get graph inputs as Stream/SidePacket
  • 使用Graph::In/SideIn将图形输入作为Stream/SidePacket获取
  • Use Node::Out/SideOut to get node outputs as Stream/SidePacket
  • 使用Node::Out/SideOut将节点输出作为Stream/SidePacket
  • Use Stream/SidePacket::ConnectTo to connect streams and side packets to node inputs (Node::In/SideIn) and graph outputs (Graph::Out/SideOut)

    使用Stream/SidePacket::ConnectTo将流和侧数据包连接到节点输入(Node::In/SideIn)和图输出(Graph::Out/SideOut
    • There's a "shortcut" operator >> that you can use instead of ConnectTo function (E.g. x >> node.In("IN")).
    • 有一个“快捷方式”操作符>>,可以使用它来代替ConnectTo函数(例如,x>>node.In(“In”))。
  • Stream/SidePacket::Cast is used to cast stream or side packet of AnyType (E.g. Stream<AnyType> in = graph.In(0);) to a particular type

    Stream/SidePacket::Cast用于投射AnyType的流或侧包(例如Stream<AnyType> in = graph.In(0);)到特定类型
    • Using actual types instead of AnyType sets you on a better path for unleashing graph builder capabilities and improving your graphs readability.
    • 使用实际类型而不是AnyType可以为释放图生成器功能和提高图形可读性提供更好的途径。

Advanced Usage

高级用法

Utility Functions

工具函数

Let's extract inference construction code into a dedicated utility function to help for readability and code reuse:

让我们将推理构造代码提取到一个专用的实用程序函数中,以帮助提高可读性和代码重用性:

// Updates graph to run inference.
Stream<std::vector<Tensor>> RunInference(
    Stream<std::vector<Tensor>> tensors, SidePacket<TfLiteModelPtr> model,
    const InferenceCalculatorOptions::Delegate& delegate, Graph& graph) {
  auto& inference_node = graph.AddNode("InferenceCalculator");
  auto& inference_opts =
      inference_node.GetOptions<InferenceCalculatorOptions>();
  *inference_opts.mutable_delegate() = delegate;
  tensors.ConnectTo(inference_node.In("TENSORS"));
  model.ConnectTo(inference_node.SideIn("MODEL"));
  return inference_node.Out("TENSORS").Cast<std::vector<Tensor>>();
}

CalculatorGraphConfig BuildGraph() {
  Graph graph;

  // Graph inputs.
  Stream<std::vector<Tensor>> input_tensors =
      graph.In(0).SetName("input_tensors").Cast<std::vector<Tensor>>();
  SidePacket<TfLiteModelPtr> model =
      graph.SideIn(0).SetName("model").Cast<TfLiteModelPtr>();

  InferenceCalculatorOptions::Delegate delegate;
  delegate.mutable_gpu();
  Stream<std::vector<Tensor>> output_tensors =
      RunInference(input_tensors, model, delegate, graph);

  // Graph outputs.
  output_tensors.SetName("output_tensors").ConnectTo(graph.Out(0));

  return graph.GetConfig();
}

As a result, RunInference provides a clear interface stating what are the inputs/outputs and their types.

因此,RunInference提供了一个明确的接口,说明输入/输出及其类型。

It can be easily reused, e.g. it's only a few lines if you want to run an extra model inference:

它可以很容易地重复使用,例如,如果你想运行额外的模型推理,它只有几行:

  // Run first inference.
  Stream<std::vector<Tensor>> output_tensors =
      RunInference(input_tensors, model, delegate, graph);
  // Run second inference on the output of the first one.
  Stream<std::vector<Tensor>> extra_output_tensors =
      RunInference(output_tensors, extra_model, delegate, graph);

And you don't need to duplicate names and tags (InferenceCalculatorTENSORSMODEL) or introduce dedicated constants here and there - those details are localized to RunInference function.

不需要重复名称和标记(推理计算器、TENSORS、MODEL),也不需要在这里和那里引入专用的常量——这些细节都本地化到RunInference函数中。

Tip: extracting RunInference and similar functions to dedicated modules (e.g. inference.h/cc which depends on the inference calculator) enables reuse in graphs construction code and helps automatically pull in calculator dependencies (e.g. no need to manually add :inference_calculator dep, just let your IDE include inference.h and build cleaner pull in corresponding dependency).

提示:将RunInference和类似函数提取到专用模块中(例如,依赖推理计算器的inference.h/cc),可以在图构建代码中重用,并有助于自动引入计算器依赖项(例如,无需手动添加:inference_calculator dep,只需让IDE包含inference.h并构建更干净的引入相应依赖项)。

Utility Classes

实用类

And surely, it's not only about functions, in some cases it's beneficial to introduce utility classes which can help making your graph construction code more readable and less error prone.

当然,这不仅仅是关于函数,在某些情况下,引入实用程序类是有益的,这有助于使图构建代码更可读,更不容易出错。

MediaPipe offers PassThroughCalculator calculator, which is simply passing through its inputs:

MediaPipe提供PassThroughCalculator计算器,它只需通过其输入:

input_stream: "float_value"
input_stream: "int_value"
input_stream: "bool_value"

output_stream: "passed_float_value"
output_stream: "passed_int_value"
output_stream: "passed_bool_value"

node {
  calculator: "PassThroughCalculator"
  input_stream: "float_value"
  input_stream: "int_value"
  input_stream: "bool_value"
  # The order must be the same as for inputs (or you can use explicit indexes)
  output_stream: "passed_float_value"
  output_stream: "passed_int_value"
  output_stream: "passed_bool_value"
}

Let's see the straightforward C++ construction code to create the above graph:

让我们看一下创建上图的简单C++构造代码:

CalculatorGraphConfig BuildGraph() {
  Graph graph;

  // Graph inputs.
  Stream<float> float_value = graph.In(0).SetName("float_value").Cast<float>();
  Stream<int> int_value = graph.In(1).SetName("int_value").Cast<int>();
  Stream<bool> bool_value = graph.In(2).SetName("bool_value").Cast<bool>();

  auto& pass_node = graph.AddNode("PassThroughCalculator");
  float_value.ConnectTo(pass_node.In("")[0]);
  int_value.ConnectTo(pass_node.In("")[1]);
  bool_value.ConnectTo(pass_node.In("")[2]);
  Stream<float> passed_float_value = pass_node.Out("")[0].Cast<float>();
  Stream<int> passed_int_value = pass_node.Out("")[1].Cast<int>();
  Stream<bool> passed_bool_value = pass_node.Out("")[2].Cast<bool>();

  // Graph outputs.
  passed_float_value.SetName("passed_float_value").ConnectTo(graph.Out(0));
  passed_int_value.SetName("passed_int_value").ConnectTo(graph.Out(1));
  passed_bool_value.SetName("passed_bool_value").ConnectTo(graph.Out(2));

  // Get `CalculatorGraphConfig` to pass it into `CalculatorGraph`
  return graph.GetConfig();
}

While pbtxt representation maybe error prone (when we have many inputs to pass through), C++ code looks even worse: repeated empty tags and Cast calls. Let's see how we can do better by introducing a PassThroughNodeBuilder:

虽然pbtxt表示可能容易出错(当我们有很多输入要通过时),但C++代码看起来更糟糕:重复的空标记和Cast调用。让我们看看如何通过引入PassThroughNodeBuilder来做得更好:

class PassThroughNodeBuilder {
 public:
  explicit PassThroughNodeBuilder(Graph& graph)
      : node_(graph.AddNode("PassThroughCalculator")) {}

  template <typename T>
  Stream<T> PassThrough(Stream<T> stream) {
    stream.ConnectTo(node_.In(index_));
    return node_.Out(index_++).Cast<T>();
  }

 private:
  int index_ = 0;
  GenericNode& node_;
};

And now graph construction code can look like:

现在图构造代码可以如下所示:

CalculatorGraphConfig BuildGraph() {
  Graph graph;

  // Graph inputs.
  Stream<float> float_value = graph.In(0).SetName("float_value").Cast<float>();
  Stream<int> int_value = graph.In(1).SetName("int_value").Cast<int>();
  Stream<bool> bool_value = graph.In(2).SetName("bool_value").Cast<bool>();

  PassThroughNodeBuilder pass_node_builder(graph);
  Stream<float> passed_float_value = pass_node_builder.PassThrough(float_value);
  Stream<int> passed_int_value = pass_node_builder.PassThrough(int_value);
  Stream<bool> passed_bool_value = pass_node_builder.PassThrough(bool_value);

  // Graph outputs.
  passed_float_value.SetName("passed_float_value").ConnectTo(graph.Out(0));
  passed_int_value.SetName("passed_int_value").ConnectTo(graph.Out(1));
  passed_bool_value.SetName("passed_bool_value").ConnectTo(graph.Out(2));

  // Get `CalculatorGraphConfig` to pass it into `CalculatorGraph`
  return graph.GetConfig();
}

Now you can't have incorrect order or index in your pass through construction code and save some typing by guessing the type for Cast from the PassThrough input.

现在,不能在直通构造代码中有不正确的顺序或索引,并通过从直通输入中猜测Cast的类型来保存一些类型。

Tip: the same as for the RunInference function, extracting PassThroughNodeBuilder and similar utility classes into dedicated modules enables reuse in graph construction code and helps to automatically pull in the corresponding calculator dependencies.

提示:与RunInference函数相同,将PassThroughNodeBuilder和类似的实用程序类提取到专用模块中可以在图构建代码中重用,并有助于自动引入相应的计算器依赖项。

Dos and Don'ts

注意事项

Define graph inputs at the very beginning if possible

如果可能的话,从一开始就定义图输入

In the code below:

在下面的代码中:

  • It can be hard to guess how many inputs you have in the graph.
  • 很难猜测在图中有多少输入。
  • Can be error prone overall and hard to maintain in future (e.g. is it a correct index? name? what if some inputs are removed or made optional? etc.).
  • 总体上可能容易出错,将来很难维护(例如,它是一个正确的索引吗?名称?如果一些输入被删除或变成可选的怎么办?等等)。
  • RunSomething reuse is limited because other graphs may have different inputs
  • RunSomething重用受到限制,因为其他图可能有不同的输入

DON'T — example of bad code.

不要做——错误代码的例子。

Stream<D> RunSomething(Stream<A> a, Stream<B> b, Graph& graph) {
  Stream<C> c = graph.In(2).SetName("c").Cast<C>();  // Bad.
  // ...
}

CalculatorGraphConfig BuildGraph() {
  Graph graph;

  Stream<A> a = graph.In(0).SetName("a").Cast<A>();
  // 10/100/N lines of code.
  Stream<B> b = graph.In(1).SetName("b").Cast<B>()  // Bad.
  Stream<D> d = RunSomething(a, b, graph);
  // ...

  return graph.GetConfig();
}

Instead, define your graph inputs at the very beginning of your graph builder:

相反,在图形生成器的一开始就定义图输入:

DO — example of good code.

DO——好代码的示例。

Stream<D> RunSomething(Stream<A> a, Stream<B> b, Stream<C> c, Graph& graph) {
  // ...
}

CalculatorGraphConfig BuildGraph() {
  Graph graph;

  // Inputs.
  Stream<A> a = graph.In(0).SetName("a").Cast<A>();
  Stream<B> b = graph.In(1).SetName("b").Cast<B>();
  Stream<C> c = graph.In(2).SetName("c").Cast<C>();

  // 10/100/N lines of code.
  Stream<D> d = RunSomething(a, b, c, graph);
  // ...

  return graph.GetConfig();
}

Use std::optional if you have an input stream or side packet that is not always defined and put it at the very beginning:

如果输入流或侧数据包并不总是定义的,请使用std::optional,并将其放在最开始:

DO — example of good code.

DO——好代码的示例。

std::optional<Stream<A>> a;
if (needs_a) {
  a = graph.In(0).SetName(a).Cast<A>();
}

Note: of course, there can be exceptions - for example, there can be a use case where calling RunSomething1(..., graph), ..., RunSomethingN(..., graph) is intended to add new inputs, so afterwards you can iterate over them and feed only added inputs into the graph. However, in any case, try to make it easy for readers to find out what graph inputs it has or may have.

注意:当然,也可能有例外——例如,可能有一个用例调用RunSomething1(..., graph), ..., RunSomethingN(..., graph)旨在添加新的输入,因此之后可以对它们进行迭代,并仅将添加的输入馈送到图中。然而,在任何情况下,都要尽量让读者更容易地了解它有什么或可能有什么图输入。

Define graph outputs at the very end

在最后定义图输出

In the code below:

在下面的代码中:

  • It can be hard to guess how many outputs you have in the graph.
  • 很难猜测在图中有多少输出。
  • Can be error prone overall and hard to maintain in future (e.g. is it a correct index? name? what if some outpus are removed or made optional? etc.).
  • 总体上可能容易出错,将来很难维护(例如,它是一个正确的索引吗?名称?如果一些输出被删除或变成可选的怎么办?等等)。
  • RunSomething reuse is limited as other graphs may have different outputs
  • RunSomething重用受到限制,因为其他图可能具有不同的输出

DON'T — example of bad code.

不要做——错误代码的例子。

void RunSomething(Stream<Input> input, Graph& graph) {
  // ...
  node.Out("OUTPUT_F")
      .SetName("output_f").ConnectTo(graph.Out(2));  // Bad.
}

CalculatorGraphConfig BuildGraph() {
  Graph graph;

  // 10/100/N lines of code.
  node.Out("OUTPUT_D")
      .SetName("output_d").ConnectTo(graph.Out(0));  // Bad.
  // 10/100/N lines of code.
  node.Out("OUTPUT_E")
      .SetName("output_e").ConnectTo(graph.Out(1));  // Bad.
  // 10/100/N lines of code.
  RunSomething(input, graph);
  // ...

  return graph.GetConfig();
}

Instead, define your graph outputs at the very end of your graph builder:

相反,在图生成器的最后定义图输出:

DO — example of good code.

DO——好代码的示例。

Stream<F> RunSomething(Stream<Input> input, Graph& graph) {
  // ...
  return node.Out("OUTPUT_F").Cast<F>();
}

CalculatorGraphConfig BuildGraph() {
  Graph graph;

  // 10/100/N lines of code.
  Stream<D> d = node.Out("OUTPUT_D").Cast<D>();
  // 10/100/N lines of code.
  Stream<E> e = node.Out("OUTPUT_E").Cast<E>();
  // 10/100/N lines of code.
  Stream<F> f = RunSomething(input, graph);
  // ...

  // Outputs.
  d.SetName("output_d").ConnectTo(graph.Out(0));
  e.SetName("output_e").ConnectTo(graph.Out(1));
  f.SetName("output_f").ConnectTo(graph.Out(2));

  return graph.GetConfig();
}

Keep nodes decoupled from each other

保持节点之间的解耦

In MediaPipe, packet streams and side packets are as meaningful as processing nodes. And any node input requirements and output products are expressed clearly and independently in terms of the streams and side packets it consumes and produces.

在MediaPipe中,数据包流和侧数据包与处理节点一样有意义。任何节点的输入需求和输出产品都以其消耗和产生的流和侧包的形式清晰而独立地表示。

DON'T — example of bad code.

不要做——错误代码的例子。

CalculatorGraphConfig BuildGraph() {
  Graph graph;

  // Inputs.
  Stream<A> a = graph.In(0).Cast<A>();

  auto& node1 = graph.AddNode("Calculator1");
  a.ConnectTo(node1.In("INPUT"));

  auto& node2 = graph.AddNode("Calculator2");
  node1.Out("OUTPUT").ConnectTo(node2.In("INPUT"));  // Bad.

  auto& node3 = graph.AddNode("Calculator3");
  node1.Out("OUTPUT").ConnectTo(node3.In("INPUT_B"));  // Bad.
  node2.Out("OUTPUT").ConnectTo(node3.In("INPUT_C"));  // Bad.

  auto& node4 = graph.AddNode("Calculator4");
  node1.Out("OUTPUT").ConnectTo(node4.In("INPUT_B"));  // Bad.
  node2.Out("OUTPUT").ConnectTo(node4.In("INPUT_C"));  // Bad.
  node3.Out("OUTPUT").ConnectTo(node4.In("INPUT_D"));  // Bad.

  // Outputs.
  node1.Out("OUTPUT").SetName("b").ConnectTo(graph.Out(0));  // Bad.
  node2.Out("OUTPUT").SetName("c").ConnectTo(graph.Out(1));  // Bad.
  node3.Out("OUTPUT").SetName("d").ConnectTo(graph.Out(2));  // Bad.
  node4.Out("OUTPUT").SetName("e").ConnectTo(graph.Out(3));  // Bad.

  return graph.GetConfig();
}

In the above code:

在上述代码中:

  • Nodes are coupled to each other, e.g. node4 knows where its inputs are coming from (node1node2node3) and it complicates refactoring, maintenance and code reuse

    节点相互耦合,例如,node4知道其输入来自何处(node1、node2、node3),这使重构、维护和代码重用变得复杂
    • Such usage pattern is a downgrade from proto representation, where nodes are decoupled by default.
    • 这种使用模式是对proto表示的降级,proto表示默认情况下节点是解耦的。
  • node#.Out("OUTPUT") calls are duplicated and readability suffers as you could use cleaner names instead and also provide an actual type.
  • 节点#.Out(“OUTPUT”)调用是重复的,可读性受到影响,因为可以使用更干净的名称,同时还提供实际的类型。

So, to fix the above issues you can write the following graph construction code:

因此,要解决上述问题,可以编写以下图构建代码:

DO — example of good code.

DO——好代码的示例。

CalculatorGraphConfig BuildGraph() {
  Graph graph;

  // Inputs.
  Stream<A> a = graph.In(0).Cast<A>();

  // `node1` usage is limited to 3 lines below.
  auto& node1 = graph.AddNode("Calculator1");
  a.ConnectTo(node1.In("INPUT"));
  Stream<B> b = node1.Out("OUTPUT").Cast<B>();

  // `node2` usage is limited to 3 lines below.
  auto& node2 = graph.AddNode("Calculator2");
  b.ConnectTo(node2.In("INPUT"));
  Stream<C> c = node2.Out("OUTPUT").Cast<C>();

  // `node3` usage is limited to 4 lines below.
  auto& node3 = graph.AddNode("Calculator3");
  b.ConnectTo(node3.In("INPUT_B"));
  c.ConnectTo(node3.In("INPUT_C"));
  Stream<D> d = node3.Out("OUTPUT").Cast<D>();

  // `node4` usage is limited to 5 lines below.
  auto& node4 = graph.AddNode("Calculator4");
  b.ConnectTo(node4.In("INPUT_B"));
  c.ConnectTo(node4.In("INPUT_C"));
  d.ConnectTo(node4.In("INPUT_D"));
  Stream<E> e = node4.Out("OUTPUT").Cast<E>();

  // Outputs.
  b.SetName("b").ConnectTo(graph.Out(0));
  c.SetName("c").ConnectTo(graph.Out(1));
  d.SetName("d").ConnectTo(graph.Out(2));
  e.SetName("e").ConnectTo(graph.Out(3));

  return graph.GetConfig();
}

Now, if needed, you can easily remove node1 and make b a graph input and no updates are needed to node2node3node4 (same as in proto representation by the way), because they are decoupled from each other.

现在,如果需要,可以很容易地删除node1并使b成为图输入,并且不需要对node2、node3、node4进行更新(顺便说一句,与proto表示中的相同),因为它们彼此解耦。

Overall, the above code replicates the proto graph more closely:

总体而言,上述代码更紧密地复制了原型图:

input_stream: "a"

node {
  calculator: "Calculator1"
  input_stream: "INPUT:a"
  output_stream: "OUTPUT:b"
}

node {
  calculator: "Calculator2"
  input_stream: "INPUT:b"
  output_stream: "OUTPUT:C"
}

node {
  calculator: "Calculator3"
  input_stream: "INPUT_B:b"
  input_stream: "INPUT_C:c"
  output_stream: "OUTPUT:d"
}

node {
  calculator: "Calculator4"
  input_stream: "INPUT_B:b"
  input_stream: "INPUT_C:c"
  input_stream: "INPUT_D:d"
  output_stream: "OUTPUT:e"
}

output_stream: "b"
output_stream: "c"
output_stream: "d"
output_stream: "e"

On top of that, now you can extract utility functions for further reuse in other graphs:

除此之外,现在可以提取实用程序函数,以便在其他图中进一步重用:

DO — example of good code.

DO——好代码的示例。

Stream<B> RunCalculator1(Stream<A> a, Graph& graph) {
  auto& node = graph.AddNode("Calculator1");
  a.ConnectTo(node.In("INPUT"));
  return node.Out("OUTPUT").Cast<B>();
}

Stream<C> RunCalculator2(Stream<B> b, Graph& graph) {
  auto& node = graph.AddNode("Calculator2");
  b.ConnectTo(node.In("INPUT"));
  return node.Out("OUTPUT").Cast<C>();
}

Stream<D> RunCalculator3(Stream<B> b, Stream<C> c, Graph& graph) {
  auto& node = graph.AddNode("Calculator3");
  b.ConnectTo(node.In("INPUT_B"));
  c.ConnectTo(node.In("INPUT_C"));
  return node.Out("OUTPUT").Cast<D>();
}

Stream<E> RunCalculator4(Stream<B> b, Stream<C> c, Stream<D> d, Graph& graph) {
  auto& node = graph.AddNode("Calculator4");
  b.ConnectTo(node.In("INPUT_B"));
  c.ConnectTo(node.In("INPUT_C"));
  d.ConnectTo(node.In("INPUT_D"));
  return node.Out("OUTPUT").Cast<E>();
}

CalculatorGraphConfig BuildGraph() {
  Graph graph;

  // Inputs.
  Stream<A> a = graph.In(0).Cast<A>();

  Stream<B> b = RunCalculator1(a, graph);
  Stream<C> c = RunCalculator2(b, graph);
  Stream<D> d = RunCalculator3(b, c, graph);
  Stream<E> e = RunCalculator4(b, c, d, graph);

  // Outputs.
  b.SetName("b").ConnectTo(graph.Out(0));
  c.SetName("c").ConnectTo(graph.Out(1));
  d.SetName("d").ConnectTo(graph.Out(2));
  e.SetName("e").ConnectTo(graph.Out(3));

  return graph.GetConfig();
}

Separate nodes for better readability

分离节点以提高可读性

DON'T — example of bad code.

不要做——错误代码的例子。

CalculatorGraphConfig BuildGraph() {
  Graph graph;

  // Inputs.
  Stream<A> a = graph.In(0).Cast<A>();
  auto& node1 = graph.AddNode("Calculator1");
  a.ConnectTo(node1.In("INPUT"));
  Stream<B> b = node1.Out("OUTPUT").Cast<B>();
  auto& node2 = graph.AddNode("Calculator2");
  b.ConnectTo(node2.In("INPUT"));
  Stream<C> c = node2.Out("OUTPUT").Cast<C>();
  auto& node3 = graph.AddNode("Calculator3");
  b.ConnectTo(node3.In("INPUT_B"));
  c.ConnectTo(node3.In("INPUT_C"));
  Stream<D> d = node3.Out("OUTPUT").Cast<D>();
  auto& node4 = graph.AddNode("Calculator4");
  b.ConnectTo(node4.In("INPUT_B"));
  c.ConnectTo(node4.In("INPUT_C"));
  d.ConnectTo(node4.In("INPUT_D"));
  Stream<E> e = node4.Out("OUTPUT").Cast<E>();
  // Outputs.
  b.SetName("b").ConnectTo(graph.Out(0));
  c.SetName("c").ConnectTo(graph.Out(1));
  d.SetName("d").ConnectTo(graph.Out(2));
  e.SetName("e").ConnectTo(graph.Out(3));

  return graph.GetConfig();
}

In the above code, it can be hard to grasp the idea where each node begins and ends. To improve this and help your code readers, you can simply have blank lines before and after each node:

在上面的代码中,很难理解每个节点的起点和终点。为了改进这一点并帮助代码读者,可以简单地在每个节点前后设置空行:

DO — example of good code.

DO——好代码的示例。

CalculatorGraphConfig BuildGraph() {
  Graph graph;

  // Inputs.
  Stream<A> a = graph.In(0).Cast<A>();

  auto& node1 = graph.AddNode("Calculator1");
  a.ConnectTo(node1.In("INPUT"));
  Stream<B> b = node1.Out("OUTPUT").Cast<B>();

  auto& node2 = graph.AddNode("Calculator2");
  b.ConnectTo(node2.In("INPUT"));
  Stream<C> c = node2.Out("OUTPUT").Cast<C>();

  auto& node3 = graph.AddNode("Calculator3");
  b.ConnectTo(node3.In("INPUT_B"));
  c.ConnectTo(node3.In("INPUT_C"));
  Stream<D> d = node3.Out("OUTPUT").Cast<D>();

  auto& node4 = graph.AddNode("Calculator4");
  b.ConnectTo(node4.In("INPUT_B"));
  c.ConnectTo(node4.In("INPUT_C"));
  d.ConnectTo(node4.In("INPUT_D"));
  Stream<E> e = node4.Out("OUTPUT").Cast<E>();

  // Outputs.
  b.SetName("b").ConnectTo(graph.Out(0));
  c.SetName("c").ConnectTo(graph.Out(1));
  d.SetName("d").ConnectTo(graph.Out(2));
  e.SetName("e").ConnectTo(graph.Out(3));

  return graph.GetConfig();
}

Also, the above representation matches CalculatorGraphConfig proto representation better.

此外,上面的表示与CalculatorGraphConfig原型表示更匹配。

If you extract nodes into utility functions, they are scoped within functions already and it's clear where they begin and end, so it's completely fine to have:

如果将节点提取到实用程序函数中,则它们已经在函数中确定了作用域,并且很清楚它们的开始和结束位置,因此完全可以:

DO — example of good code.

DO——好代码的示例。

CalculatorGraphConfig BuildGraph() {
  Graph graph;

  // Inputs.
  Stream<A> a = graph.In(0).Cast<A>();

  Stream<B> b = RunCalculator1(a, graph);
  Stream<C> c = RunCalculator2(b, graph);
  Stream<D> d = RunCalculator3(b, c, graph);
  Stream<E> e = RunCalculator4(b, c, d, graph);

  // Outputs.
  b.SetName("b").ConnectTo(graph.Out(0));
  c.SetName("c").ConnectTo(graph.Out(1));
  d.SetName("d").ConnectTo(graph.Out(2));
  e.SetName("e").ConnectTo(graph.Out(3));

  return graph.GetConfig();
}

  • 0
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值