الانطباع الأول عن المفاهيم



قررت التعامل مع ميزة C ++ 20 الجديدة - المفاهيم.

المفاهيم (أو المفاهيم ، كما يكتب ويكي الناطقين بالروسية) هي ميزة مثيرة للاهتمام ومفيدة للغاية كانت موجودة منذ فترة طويلة.

بشكل أساسي ، يتم الكتابة لوسيطة القالب.

المشكلة الرئيسية في القوالب قبل C ++ 20 هي أنه يمكنك استبدال أي شيء فيها ، بما في ذلك شيء لم يتم تصميمه من أجله على الإطلاق. أي أن نظام القوالب لم يتم حله بالكامل. ونتيجة لذلك ، حدثت رسائل خطأ طويلة للغاية وغير قابلة للقراءة تمامًا عند تمرير المعلمة الخاطئة إلى القالب. لقد حاولوا محاربة هذا بمساعدة الاختراقات اللغوية المختلفة ، والتي لا أريد حتى أن أذكرها (على الرغم من أنني اضطررت إلى التعامل معها).

المفاهيم مصممة لتصحيح سوء الفهم هذا. يضيفون نظامًا للكتابة إلى القوالب ، وهو قوي جدًا. والآن ، بفهم ميزات هذا النظام ، بدأت في دراسة المواد المتاحة على الإنترنت.

بصراحة ، أنا مصدوم قليلاً :) لغة C ++ هي لغة معقدة بالفعل ، ولكن على الأقل هناك عذر: حدث ذلك. تم اكتشاف البرمجة التخطيطية على القوالب ، وليس وضعها ، عند تصميم لغة. وبعد ذلك ، عند تطوير الإصدارات التالية من اللغة ، أجبروا على التكيف مع هذا "الاكتشاف" ، حيث تم كتابة الكثير من التعليمات البرمجية في العالم. المفاهيم هي فرصة جديدة بشكل أساسي. ويبدو لي أن بعض التعتيم موجود بالفعل في تنفيذها. ولعل هذا نتيجة للحاجة إلى مراعاة الكم الهائل من القدرات الموروثة؟ دعونا نحاول معرفة ذلك ...

معلومات عامة


المفهوم هو كيان لغوي جديد يعتمد على بنية القالب. يحتوي المفهوم على اسم ومعلمات وجسم - وهو مُسند يُرجع قيمة منطقية ثابتة (أي محسوبة في مرحلة التجميع) اعتمادًا على معلمات المفهوم. مثله:

template<int I> 
concept Even = I % 2 == 0;  

template<typename T>
concept FourByte = sizeof(T)==4;

من الناحية الفنية ، تتشابه المفاهيم كثيرًا مع تعبيرات قالب constexpr مثل bool:

template<int I>
constexpr bool EvenX = I % 2 == 0; 

template<typename T>
constexpr bool FourByteX = sizeof(T)==4;

يمكنك حتى استخدام المفاهيم في التعبيرات الشائعة:

bool b1 = Even<2>; 

باستخدام


الفكرة الرئيسية للمفاهيم هي أنه يمكن استخدامها بدلاً من الكلمات الرئيسية للفئة في القالب. مثل metatypes ("أنواع الأنواع"). وبالتالي ، يتم إدخال الكتابة الثابتة في القوالب.

template<FourByte T>
void foo(T const & t) {}

الآن ، إذا استخدمنا int كمعلمة قالب ، فسيتم تجميع التعليمات البرمجية في الغالبية العظمى من الحالات ؛ وإذا كانت مزدوجة ، فسيتم إصدار رسالة خطأ قصيرة ومفهومة. كتابة قوالب بسيطة وواضحة ، كل شيء على ما يرام حتى الآن.

يستوجب


هذه كلمة رئيسية جديدة "سياقية" لـ C ++ 20 ذات غرض مزدوج: تتطلب عبارة وتتطلب تعبيرًا. كما سيظهر لاحقًا ، يؤدي توفير الكلمات الرئيسية الغريبة إلى بعض الارتباك.

يتطلب التعبير


أولاً ، يتطلب التفكير في التعبير. الفكرة جيدة جدًا: تحتوي هذه الكلمة على كتلة في الأقواس ، ويتم تقييم الشفرة الموجودة بداخلها لتجميعها. صحيح ، لا يجب كتابة الشفرة هناك بلغة C ++ ، ولكن بلغة خاصة ، قريبة من C ++ ، ولكن لها خصائصها الخاصة (هذه هي الغرابة الأولى ، كان من الممكن جدًا إنشاء رمز C ++ فقط).

إذا كان الرمز صحيحًا - يتطلب التعبير إرجاع صحيح ، وإلا خطأ. الكود نفسه ، بالطبع ، لا يدخل أبدًا في إنشاء الكود أبدًا ، مثل الكثير من التعبيرات في حجم أو نوع.

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

bool b = requires { 3.14 >> 1; };

وفي النموذج - يرجى:

template<typename T>
constexpr bool Shiftable = requires(T i) { i>>1; };

وستعمل:

bool b1 = Shiftable<int>; // true
bool b2 = Shiftable<double>; // false

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

template <typename T>
concept Machine = 
  requires(T m) {  //   `m` ,   Machine
	m.start();     //    `m.start()` 
	m.stop();      //   `m.stop()`
};  

بالمناسبة ، يجب التصريح عن جميع المتغيرات التي قد تكون مطلوبة في التعليمات البرمجية التي تم اختبارها (ليس فقط معلمات القالب) بين قوسين يتطلب التعبير. لسبب ما ، الإعلان عن متغير غير ممكن ببساطة.

يتطلب فحص نوع داخل


هذا هو المكان الذي تبدأ فيه الاختلافات بين يتطلب رمز و C ++ القياسي. للتحقق من الأنواع التي تم إرجاعها ، يتم استخدام بناء جملة خاص: يتم أخذ الكائن بين قوسين مجعدين ، ويتم وضع سهم ، وبعد ذلك ، يتم كتابة مفهوم يجب أن يفي به النوع. علاوة على ذلك ، لا يُسمح باستخدام أنواع مباشرة.

تحقق من إمكانية تحويل إرجاع الدالة إلى int:

requires(T v, int i) {
  { v.f(i) } -> std::convertible_to<int>;
}  

تحقق من أن دالة الإرجاع صحيحة تمامًا:

requires(T v, int i) {
  { v.f(i) } -> std::same_as<int>; 
}  

(std :: same_as و std :: convertible_to مفاهيم من المكتبة القياسية).

إذا لم تقم بتضمين تعبير تم فحص نوعه بأقواس ، فإن المترجم لا يفهم ما يريده منه ويفسر السلسلة بأكملها على أنها تعبير واحد يحتاج إلى التحقق من التجميع.

يتطلب الداخل يتطلب


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

requires { 
  expression;         // expression is valid
  requires predicate; // predicate is true
};

كمسند ، على سبيل المثال ، يمكن استخدام المفاهيم المحددة أو سمات النوع. مثال:

requires(Iter it) {
  //     (   Iter   *  ++)
  *it++;
 
  //    -  
  requires std::convertible_to<decltype(*it++), typename Iter::value_type>;
 
  //    -  
  requires std::is_convertible_v<decltype(*it++), typename Iter::value_type>;
}

في الوقت نفسه ، يُسمح باستخدام عبارات متداخلة - متداخلة برمز بين أقواس معقوفة ، والتي يتم التحقق من صحتها. ومع ذلك ، إذا قمت ببساطة بكتابة تعبير مطلوب واحد داخل آخر ، فسيتم التحقق من التعبير المتداخل (كل شيء ككل ، بما في ذلك الكلمة الأساسية المتداخلة) للتحقق من صلاحيتها:

requires (T v) { 
  requires (typename T::value_type x) { ++x; }; //     , 
												//     !
};  

لذلك ، نشأ شكل غريب مع مضاعفة يتطلب:

requires (T v) { 
  requires requires (typename T::value_type x) { ++x; }; //       "++x"
};  

هنا تسلسل هروب ممتع من "يتطلب".

بالمناسبة ، هناك مزيج آخر من اثنين يتطلب هذا البند الزمني (انظر أدناه) والتعبير:

template <typename T>
  requires requires(T x, T y) { bool(x < y); }
bool equivalent(T const& x, T const& y)
{
  return !(x < y) && !(y < x);
};

يتطلب فقرة


الآن دعنا ننتقل إلى استخدام آخر للكلمة يتطلب - لإعلان قيود نوع القالب. هذا بديل لاستخدام أسماء المفاهيم بدلاً من اسم الكتابة. في المثال التالي ، جميع الطرق الثلاثة متكافئة:

//  require
template<typename Cont>
	requires Sortable<Cont>
void sort(Cont& container);

//   require (  )
template<typename Cont>
void sort(Cont& container) requires Sortable<Cont>;

//    typename
template<Sortable Cont>
void sort(Cont& container)  

يمكن أن يستخدم التصريح المطلوب العديد من المسندات مجتمعة بواسطة عوامل تشغيل منطقية.

template <typename T>
  requires is_standard_layout_v<T> && is_trivial_v<T>
void fun(T v); 
 
int main()
{
  std::string s;
 
  fun(1);  // ok
  fun(s);  // compiler error
}

ومع ذلك ، ما عليك سوى عكس أحد الشروط ، حيث يحدث خطأ في الترجمة:

template <typename T>
  requires is_standard_layout_v<T> && !is_trivial_v<T>
void fun(T v); 

هنا مثال لن يتم تجميعه أيضًا

template <typename T>
  requires !is_trivial_v<T>
void fun(T v);	

والسبب في ذلك هو الغموض الذي يظهر عند تحليل بعض التعبيرات. على سبيل المثال ، في مثل هذا القالب:

template <typename T> 
  requires (bool)&T::operator short unsigned int foo();

من غير الواضح ما الذي يُنسب إليه - عامل التشغيل أو النموذج الأولي لوظيفة foo (). لذلك ، قرر المطورون أنه بدون قوسين ، حيث تتطلب الوسيطات فقرة ، لا يمكن استخدام سوى مجموعة محدودة جدًا من الكيانات - الحرف الحقيقي أو الكاذب ، وأسماء الحقول من نوع القيمة المنطقية للقيمة النموذجية ، والقيمة ، T :: القيمة ، ns :: trait :: value ، أسماء المفاهيم من نوع المفهوم وتتطلب تعابير. يجب وضع كل شيء آخر بين قوسين:

template <typename T>
  requires (!is_trivial_v<T>)
void fun(T v);

الآن حول الميزات المسند في يتطلب جملة


تأمل في مثال آخر.

template <typename T>
  requires is_trivial_v<typename T::value_type> 
void fun(T v); 

في هذا المثال ، يتطلب استخدام سمة تعتمد على نوع القيمة المتداخلة. ليس من المعروف مسبقًا ما إذا كان للنوع التعسفي نوع متداخل يمكن تمريره إلى القالب. إذا مررت ، على سبيل المثال ، نوع int بسيط لمثل هذا القالب ، فسيكون هناك خطأ في الترجمة ، ومع ذلك ، إذا كان لدينا تخصصان للقالب ، فلن يكون هناك خطأ ؛ سيتم اختيار تخصص آخر فقط.

template <typename T>
  requires is_trivial_v<typename T::value_type> 
void fun(T v) { std::cout << "1"; } 
 
template <typename T>
void fun(T v) { std::cout << "2"; } 
 
int main()
{
  fun(1);  // displays: "2"
}

وبالتالي ، يتم تجاهل التخصص ليس فقط عندما ترجع جملة الشرط المطلوبة خطأ ، ولكن أيضًا عندما يتبين أنها غير صحيحة.

الأقواس حول المسند تذكير هام بأنه في شرط يتطلب عكس معكوس المسند ليس عكس المسند نفسه. وبالتالي،

requires is_trivial_v<typename T::value_type> 

يعني أن السمة صحيحة وتعود صحيحة. حيث

!is_trivial_v<typename T::value_type> 

قد تعني "السمة صحيحة وترجع خطأ"
الانعكاس المنطقي الحقيقي للمسند الأول ليس ("السمة صحيحة وترجع صحيحة") == "السمة غير صحيحة أو ترجع خطأ" - يتم تحقيق ذلك بطريقة أكثر تعقيدًا قليلاً - من خلال تعريف صريح للمفهوم:

template <typename T>
concept value_type_valid_and_trivial 
  = is_trivial_v<typename T::value_type>; 
 
template <typename T>
  requires (!value_type_valid_and_trivial<T>)
void fun(T v); 

التزامن والانفصال


تبدو عوامل الاقتران والارتباط المنطقية كالمعتاد ، ولكنها تعمل في الواقع بشكل مختلف قليلاً عن العادي في C ++.

فكر في اثنين من مقتطفات الشفرة المتشابهة جدًا.

الأول هو المسند بدون قوسين:

template <typename T, typename U>
  requires std::is_trivial_v<typename T::value_type>
		|| std::is_trivial_v<typename U::value_type>
void fun(T v, U u); 

والثاني مع الأقواس:

template <typename T, typename U>
  requires (std::is_trivial_v<typename T::value_type>
		 || std::is_trivial_v<typename U::value_type>)
void fun(T v, U u); 

الفرق هو فقط بين قوسين. ولكن بسبب هذا ، في القالب الثاني ، لا توجد قيودان يوحدهما "بند شرط" ، ولكن هناك قيود توحدها OR المنطقية المعتادة.

هذا الاختلاف هو على النحو التالي. خذ بعين الاعتبار الرمز

std::optional<int> oi {};
int i {};
fun(i, oi);

هنا يتم إنشاء القالب بواسطة أنواع int و std :: اختيارية.

في الحالة الأولى ، نوع int :: value_type غير صالح ، وبالتالي لا يتم استيفاء الحد الأول.

ولكن النوع الاختياري :: value_type صالح ، وتعرض السمة الثانية صوابًا ، وبما أن هناك عامل تشغيل OR بين القيود ، فإن المسند بأكمله راضٍ ككل.

في الحالة الثانية ، هذا تعبير واحد يحتوي على نوع غير صالح ، لأنه غير صالح بشكل عام والمسند غير راضٍ. حتى أقواس بسيطة غير محسوس معنى ما يحدث.

فى الختام


بالطبع ، لا يتم عرض جميع ميزات المفاهيم هنا. أنا لم أذهب أبعد من ذلك. ولكن كأنطباع أول - فكرة مثيرة للاهتمام وتنفيذ غريب إلى حد ما. ويتطلب بناء جملة مضحكة مع التكرار ، الأمر الذي يخلط حقًا. هل هناك عدد قليل جدًا من الكلمات في اللغة الإنجليزية لدرجة أنه كان عليك استخدام كلمة واحدة لأغراض مختلفة تمامًا؟

الفكرة مع التعليمات البرمجية المترجمة جيدة بالتأكيد. حتى أنها تشبه إلى حد ما "شبه اقتباس" في وحدات ماكرو بناء الجملة. ولكن هل كان من المفيد مزج الصيغة الخاصة لفحص أنواع الإرجاع؟ IMHO ، لهذا سيكون من الضروري ببساطة إنشاء كلمة رئيسية منفصلة.

الاختلاط الضمني بين مفهومي "صواب / خطأ" و "تجميع / عدم تجميع" في كومة واحدة ، ونتيجة لذلك ، فإن النكات ذات الأقواس خاطئة أيضًا. هذه مفاهيم مختلفة ، ويجب أن تكون موجودة بشكل صارم في سياقات مختلفة (على الرغم من أنني أفهم من أين جاءت - من قاعدة SFINAE ، حيث تم استبعاد التعليمات البرمجية غير المجمعة بصمت التخصص من الاعتبار). ولكن إذا كان الهدف من المفاهيم هو جعل الشفرة واضحة قدر الإمكان ، فهل يستحق الأمر سحب كل هذه الأشياء الضمنية إلى ميزات جديدة؟

المقالة مكتوبة بشكل أساسي على أساس
akrzemi1.wordpress.com/2020/01/29/requires-expression
akrzemi1.wordpress.com/2020/03/26/requires-clause
(هناك المزيد من الأمثلة والميزات المثيرة للاهتمام)
مع الإضافات من مصادر أخرى ،
يمكن التحقق من جميع الأمثلةwandbox.org

All Articles