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 asStream/SidePacket
使用Graph::In/SideIn将图形输入作为Stream/SidePacket获取
- Use
Node::Out/SideOut
to get node outputs asStream/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 ofConnectTo
function (E.g.x >> node.In("IN")
). - 有一个“快捷方式”操作符>>,可以使用它来代替ConnectTo函数(例如,x>>node.In(“In”))。
- There's a "shortcut" operator
Stream/SidePacket::Cast
is used to cast stream or side packet ofAnyType
(E.g.Stream<AnyType> in = graph.In(0);
) to a particular typeStream/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可以为释放图生成器功能和提高图形可读性提供更好的途径。
- Using actual types instead of
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 (InferenceCalculator
, TENSORS
, MODEL
) 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 includeinference.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, extractingPassThroughNodeBuilder
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 (node1
,node2
,node3
) 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 node2
, node3
, node4
(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();
}