إنشاء الرسوم البيانية بلغة C++

تعد منصة إنشاء الرسم البياني C++ أداة قوية من أجل:

  • إنشاء الرسوم البيانية المعقّدة
  • الرسوم البيانية التي تستخدم أساليب بارامترية (مثل ضبط تفويض في InferenceCalculator تمكين/تعطيل أجزاء من الرسم البياني)
  • تكرار الرسومات البيانية (مثلاً بدلاً من الرسوم البيانية المخصّصة لوحدة المعالجة المركزية (CPU) ووحدة معالجة الرسومات (GPU) في ملف pbtxt يمكن أن يكون لديك رمز واحد ينشئ الرسوم البيانية المطلوبة، ويشارك أكبر قدر ممكن قدر الإمكان)
  • إتاحة إدخالات/مخرجات اختيارية للرسم البياني
  • تخصيص الرسوم البيانية لكل نظام أساسي

الاستخدام الأساسي

دعونا نرى كيف يمكن استخدام أداة إنشاء الرسوم البيانية 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 {} }
    }
  }
}

قد تبدو دالة إنشاء 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();
}

ملخص قصير:

  • استخدام Graph::In/SideIn للحصول على إدخالات الرسم البياني كـ Stream/SidePacket
  • استخدام Node::Out/SideOut للحصول على مخرجات العُقد كـ Stream/SidePacket
  • استخدام Stream/SidePacket::ConnectTo لربط ساحات المشاركات وحزم البيانات الجانبية بـ مدخلات العُقد (Node::In/SideIn) ومخرجات الرسم البياني (Graph::Out/SideOut)
    • يوجد "اختصار" العامل >> الذي يمكنك استخدامه بدلاً من الدالة ConnectTo (على سبيل المثال x >> node.In("IN")).
  • تُستخدَم Stream/SidePacket::Cast لبثّ البث أو الحزمة الجانبية لـ "AnyType". (مثل Stream<AnyType> in = graph.In(0);) إلى نوع معيّن
    • يؤدي استخدام الأنواع الفعلية بدلاً من AnyType إلى وضعك على مسار أفضل إطلاق العنان لإمكانات أداة إنشاء الرسومات البيانية وتحسين الرسوم البيانية وسهولة القراءة.

الاستخدام المتقدّم

وظائف الخدمة

هيا نستخرج رمز إنشاء الاستدلال إلى دالة فائدة مخصصة المساعدة في تسهيل القراءة وإعادة استخدام الرمز البرمجي:

// 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();
}

نتيجةً لذلك، توفّر RunInference واجهة واضحة توضّح ما يلي: المدخلات/المخرجات وأنواعها.

ويمكن إعادة استخدامها بسهولة، على سبيل المثال: تتكون من بضعة أسطر فقط إذا كنت تريد تنفيذ الاستنتاج النموذجي:

  // 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);

ولا تحتاج إلى تكرار الأسماء والعلامات (InferenceCalculator، TENSORS، MODEL) أو إدخال الثوابت المخصصة هنا وهناك - تلك يتم ترجمة التفاصيل إلى الدالة RunInference.

فئات الخدمات

وبالتأكيد، لا يتعلق الأمر بالدوال فقط، بل من المفيد في بعض الحالات وتقديم فئات الخدمات التي يمكن أن تساعد في إنشاء رمز إنشاء الرسم البياني أكثر قابلية للقراءة وأقل عرضة للخطأ.

توفّر 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"
}

لنرى رمز إنشاء 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();
}

بينما قد يكون تمثيل 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_;
};

والآن يمكن أن يبدو رمز إنشاء الرسم البياني على النحو التالي:

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();
}

لا يمكن أن يكون هناك ترتيب أو فهرس غير صحيح في بطاقة المرور خلال عملية الإنشاء احفظ بعض الكتابة من خلال تخمين نوع Cast من PassThrough إدخال.

افعل ولا تفعل

تحديد إدخالات الرسم البياني في البداية إن أمكن

في الرمز البرمجي أدناه:

  • قد يكون من الصعب تخمين عدد الإدخالات في الرسم البياني.
  • يمكن أن يكون عرضة للخطأ بشكل عام ويصعب الحفاظ عليه في المستقبل (على سبيل المثال، هل هو هل فهرس صحيح؟ اسمه؟ ماذا لو تمت إزالة بعض الإدخالات أو جعلها اختيارية؟ وما إلى ذلك).
  • تم فرض قيود على إعادة استخدام RunSomething لأنّ الرسومات البيانية الأخرى قد تتضمّن بيانات مختلفة مصادر الإدخال

DON'T: مثال على رمز برمجي غير صالح

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();
}

بدلاً من ذلك، حدِّد إدخالات الرسم البياني في بداية أداة إنشاء الرسم البياني:

إجراء مطلوب: مثال على الرمز البرمجي الجيد.

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();
}

استخدم std::optional إذا كان لديك بث إدخال أو حزمة جانبية ليست كذلك دائمًا وأضعها في البداية:

إجراء مطلوب: مثال على الرمز البرمجي الجيد.

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

تحديد مخرجات الرسم البياني في النهاية

في الرمز البرمجي أدناه:

  • قد يكون من الصعب تخمين عدد المخرجات في الرسم البياني.
  • يمكن أن يكون عرضة للخطأ بشكل عام ويصعب الحفاظ عليه في المستقبل (على سبيل المثال، هل هو هل فهرس صحيح؟ اسمه؟ ماذا لو تمت إزالة بعض الخروجات أو جعلها اختيارية؟ وما إلى ذلك).
  • تم فرض قيود على إعادة استخدام RunSomething لأنّ الرسومات البيانية الأخرى قد تحتوي على مخرجات مختلفة.

DON'T: مثال على رمز برمجي غير صالح

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();
}

بدلاً من ذلك، حدِّد مخرجات الرسم البياني في نهاية أداة إنشاء الرسم البياني:

إجراء مطلوب: مثال على الرمز البرمجي الجيد.

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();
}

الحفاظ على فصل العُقد عن بعضها البعض

في MediaPipe، تكون تدفقات الحزم والحزم الجانبية بنفس أهمية المعالجة العُقد. ويتم التعبير بوضوح عن أي متطلبات إدخال للعقدة ومنتجات الإخراج وبشكل مستقل من حيث التدفقات والحزم الجانبية التي تستهلكها إنتاجه.

DON'T: مثال على رمز برمجي غير صالح

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();
}

في الرمز البرمجي أعلاه:

  • تقترن العُقد بعضها ببعض، على سبيل المثال: يعرف node4 مكان إدخالاته قادمة من (node1، node2، node3) وتؤدي إلى تعقيد إعادة البناء، الصيانة وإعادة استخدام الرمز البرمجي
    • نمط الاستخدام هذا هو الرجوع إلى إصدار سابق من تمثيل النموذج الأولي، حيث تكون العُقد يتم فصلها افتراضيًا.
  • يتم تكرار مكالمتَين (node#.Out("OUTPUT")) وتتأثر قابلية القراءة مع تقدّمك في العمل. استخدام أسماء أنظف بدلاً من ذلك وكذلك توفير نوع فعلي.

لذلك، لإصلاح المشكلات أعلاه، يمكنك كتابة كود إنشاء الرسم البياني التالي:

إجراء مطلوب: مثال على الرمز البرمجي الجيد.

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();
}

وإذا لزم الأمر، يمكنك بسهولة إزالة node1 وجعل b إدخال الرسم البياني يجب إجراء تعديلات على node2 وnode3 وnode4 (كما هو الحال في التمثيل الأوّلي). بالمناسبة)، لأنها تكون منفصلة عن بعضها بعضًا.

بشكل عام، تنسخ التعليمة البرمجية أعلاه الرسم البياني الأوّلي بشكل أوثق:

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"

علاوةً على ذلك، يمكنك الآن استخراج الدوال المساعدة لإعادة استخدامها في الرسوم البيانية الأخرى:

إجراء مطلوب: مثال على الرمز البرمجي الجيد.

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();
}

فصل العُقد لتسهيل القراءة

DON'T: مثال على رمز برمجي غير صالح

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();
}

في التعليمة البرمجية أعلاه، قد يكون من الصعب استيعاب الفكرة التي تبدأ فيها كل عقدة تنتهي. لتحسين ذلك ومساعدة قراء الرموز، يمكنك ببساطة أن يكون لديك الأسطر قبل وبعد كل عقدة:

إجراء مطلوب: مثال على الرمز البرمجي الجيد.

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();
}

يتطابق أيضًا التمثيل أعلاه مع نموذج CalculatorGraphConfig التمثيل أفضل.

إذا كنت تستخرج العُقد إلى دوال المنفعة، سيتم تحديد نطاقها ضمن الدوال بالفعل ومن الواضح أين تبدأ وتنتهي، لذلك من الجيد تمامًا تحتوي على:

إجراء مطلوب: مثال على الرمز البرمجي الجيد.

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();
}