مرحبا يا هابر! نقدم انتباهكم إلى ترجمة مقالة "كل ما تحتاج إلى معرفته عن std :: any from C ++ 17" بقلم بارتلوميج فيليبك .
بمساعدة std::optional
يمكنك تخزين نوع واحد من النوع. بمساعدة std::variant
يمكنك تخزين عدة أنواع في كائن واحد. ويوفر لنا C ++ 17 نوعًا آخر من هذه الأغلفة - std::any
يمكنه تخزين أي شيء مع الاحتفاظ بالأمان من النوع.أساسيات
قبل ذلك ، لم يقدم معيار C ++ العديد من الحلول لمشكلة تخزين عدة أنواع في متغير واحد. بالطبع يمكنك استخدامها void*
، لكنها ليست آمنة على الإطلاق.من الناحية النظرية ، void*
يمكنك لفها في فصل حيث يمكنك تخزين النوع بطريقة أو بأخرى:class MyAny
{
void* _value;
TypeInfo _typeInfo;
};
كما ترى ، حصلنا على نموذج أساسي معين std::any
، ولكن لضمان سلامة النوع ، MyAny
نحتاج إلى فحوصات إضافية. هذا هو السبب في أنه من الأفضل استخدام خيار من المكتبة القياسية بدلاً من اتخاذ قرارك الخاص.وهذا ما هو عليه std::any
من C ++ 17. يسمح لك بتخزين أي شيء في الكائن والإبلاغ عن خطأ (يطرح استثناء) عند محاولة الوصول عن طريق تحديد النوع الخاطئ.عرض صغير:std::any a(12);
a = std::string("Hello!");
a = 16;
std::cout << std::any_cast<int>(a) << '\n';
try
{
std::cout << std::any_cast<std::string>(a) << '\n';
}
catch(const std::bad_any_cast& e)
{
std::cout << e.what() << '\n';
}
a.reset();
if (!a.has_value())
{
std::cout << "a is empty!" << "\n";
}
std::map<std::string, std::any> m;
m["integer"] = 10;
m["string"] = std::string("Hello World");
m["float"] = 1.0f;
for (auto &[key, val] : m)
{
if (val.type() == typeid(int))
std::cout << "int: " << std::any_cast<int>(val) << "\n";
else if (val.type() == typeid(std::string))
std::cout << "string: " << std::any_cast<std::string>(val) << "\n";
else if (val.type() == typeid(float))
std::cout << "float: " << std::any_cast<float>(val) << "\n";
}
سيخرج هذا الرمز:16
bad any_cast
a is empty!
float: 1
int: 10
string: Hello World
يوضح المثال أعلاه بعض الأشياء المهمة:std::any
— std::optional
std::variant
- ,
.has_value()
.reset()
std::decay
- ,
std::any_cast
, bad_any_cast
, «T».type()
, std::type_info
يبدو المثال أعلاه مثيرًا للإعجاب - متغير نوع حقيقي في C ++! إذا كنت مغرمًا جدًا بجافا سكريبت ، فيمكنك حتى إنشاء جميع متغيرات الكتابة std::any
واستخدام C ++ كـ JavaScript :)ولكن ربما هناك بعض أمثلة الاستخدام العادي؟متى يجب استخدام؟
على الرغم void*
من أنني أعتبرها شيئًا غير آمن للغاية مع مجموعة محدودة جدًا من الاستخدامات المحتملة ، إلا أنها std::any
آمنة تمامًا من النوع ، لذلك لديها بعض الطرق الجيدة لاستخدامها.على سبيل المثال:- في المكتبات - عندما تحتاج مكتبتك إلى تخزين بعض البيانات أو نقلها ، ولا تعرف نوع هذه البيانات
- عند تحليل الملفات - إذا لم تتمكن حقًا من تحديد الأنواع المدعومة
- المراسلة
- تفاعل لغة البرمجة النصية
- إنشاء مترجم للغة البرمجة
- واجهة المستخدم - يمكن للحقول تخزين أي شيء
يبدو لي أنه في العديد من هذه الأمثلة يمكننا تسليط الضوء على قائمة محدودة من الأنواع المدعومة ، لذلك std::variant
قد يكون خيارًا أفضل. ولكن بالطبع من الصعب إنشاء مكتبات دون معرفة المنتجات النهائية التي سيتم استخدامها فيها. أنت فقط لا تعرف ما هي الأنواع التي سيتم تخزينها هناك.أظهر العرض التوضيحي بعض الأشياء الأساسية ، ولكن في الأقسام التالية ستتعلم المزيد عن std::any
ذلك ، لذا استمر في القراءة.إنشاء std :: any
هناك عدة طرق لإنشاء كائن من النوع std::any
:- التهيئة القياسية - الكائن فارغ
- التهيئة المباشرة بالقيمة / الكائن
- تشير مباشرة إلى نوع الكائن -
std::in_place_type
- عبر
std::make_any
على سبيل المثال:
std::any a;
assert(!a.has_value());
std::any a2(10);
std::any a3(MyType(10, 11));
std::any a4(std::in_place_type<MyType>, 10, 11);
std::any a5{std::in_place_type<std::string>, "Hello World"};
std::any a6 = std::make_any<std::string>("Hello World");
تغيير القيمة
هناك std::any
طريقتان لتغيير القيمة المخزنة حاليًا في : الطريقة emplace
أو المهمة:std::any a;
a = MyType(10, 11);
a = std::string("Hello");
a.emplace<float>(100.5f);
a.emplace<std::vector<int>>({10, 11, 12, 13});
a.emplace<MyType>(10, 11);
دورة حياة الكائن
مفتاح الأمن std::any
هو نقص تسرب الموارد. لتحقيق ذلك ، سوف std::any
يدمر أي كائن نشط قبل تعيين قيمة جديدة.std::any var = std::make_any<MyType>();
var = 100.0f;
std::cout << std::any_cast<float>(var) << "\n";
سيخرج هذا الرمز ما يلي:MyType::MyType
MyType::~MyType
100
تتم std::any
تهيئة الكائن بكائن من النوع MyType ، ولكن قبل تعيين قيمة جديدة (100.0f) ، يتم استدعاء المدمر MyType
.الوصول إلى قيمة
في معظم الحالات ، لديك طريقة واحدة فقط للوصول إلى القيمة std::any
- std::any_cast
تقوم بإرجاع قيم النوع المحدد إذا تم تخزينها في الكائن.هذه الميزة مفيدة للغاية ، حيث أن لديها العديد من الطرق لاستخدامها:- إرجاع نسخة من القيمة وإنهاء
std::bad_any_cast
الخطأ - إرجاع ارتباط إلى القيمة وإنهاء
std::bad_any_cast
الخطأ - إرجاع المؤشر إلى قيمة (ثابتة أم لا) أو nullptr في حالة حدوث خطأ
انظر مثال:struct MyType
{
int a, b;
MyType(int x, int y) : a(x), b(y) { }
void Print() { std::cout << a << ", " << b << "\n"; }
};
int main()
{
std::any var = std::make_any<MyType>(10, 10);
try
{
std::any_cast<MyType&>(var).Print();
std::any_cast<MyType&>(var).a = 11;
std::any_cast<MyType&>(var).Print();
std::any_cast<int>(var);
}
catch(const std::bad_any_cast& e)
{
std::cout << e.what() << '\n';
}
int* p = std::any_cast<int>(&var);
std::cout << (p ? "contains int... \n" : "doesn't contain an int...\n");
MyType* pt = std::any_cast<MyType>(&var);
if (pt)
{
pt->a = 12;
std::any_cast<MyType&>(var).Print();
}
}
كما ترى ، لدينا طريقتان لتتبع الأخطاء: من خلال الاستثناءات ( std::bad_any_cast
) أو إرجاع المؤشر (أو nullptr
). تم تحميل وظيفة std::any_cast
إرجاع المؤشرات بشكل زائد noexcept
.الأداء واستخدام الذاكرة
std::any
يبدو أنها أداة قوية ، وعلى الأرجح ستستخدمها لتخزين البيانات من أنواع مختلفة ، ولكن ما هو سعرها؟المشكلة الرئيسية هي تخصيص ذاكرة إضافية.std::variant
و std::optional
لا يتطلب أي مخصصات ذاكرة إضافية، ولكن هذا لأن أنواع البيانات المخزنة في وجوه معروفة مسبقا. لا يملك std :: any مثل هذه المعلومات ، لذا يمكنه استخدام ذاكرة إضافية.هل سيحدث هذا دائمًا أم أحيانًا؟ أي قواعد؟ هل سيحدث هذا حتى مع أنواع بسيطة مثل int؟دعونا نرى ما يقوله المعيار:يجب أن تتجنب عمليات التنفيذ استخدام الذاكرة المخصصة ديناميكيًا لقيمة محتواة صغيرة. مثال: حيث يحتوي الكائن الذي تم إنشاؤه على عدد صحيح فقط. يتم تطبيق تحسين الكائنات الصغيرة فقط على الأنواع T التي يكون is_othrow_move_constructible_v لها
يجب أن يتجنب التطبيق استخدام الذاكرة الديناميكية للبيانات المخزنة صغيرة الحجم. على سبيل المثال ، عندما يتم إنشاء كائن تخزين فقط int. يجب تطبيق هذا التحسين للأجسام الصغيرة فقط على الأنواع T التي يكون is_othrow_move_constructible_v لها.
نتيجة لذلك ، يقترحون استخدام Small Buffer Optimization / SBO لعمليات التنفيذ. ولكن هذا له ثمن أيضا. هذا يجعل النوع أكبر - لتغطية المخزن المؤقت.دعونا نلقي نظرة على الحجم std::any
، وإليك نتائج العديد من المترجمات:بشكل عام ، كما ترى ، std::any
هذا ليس نوعًا بسيطًا ، ويجلب تكاليف إضافية. عادة ما يستغرق الكثير من الذاكرة ، بسبب SBO ، من 16 إلى 32 بايت (في GCC أو clang ... أو حتى 64 بايت في MSVC!).الترحيل من التعزيز :: أي
boost::any
تم تقديمه في مكان ما في عام 2001 (الإصدار 1.23.0). بالإضافة إلى ذلك ، فإن المؤلف boost::any
(Kevlin Henney) هو أيضًا صاحب الاقتراح std::any
. لذلك ، يرتبط هذان النوعان ارتباطًا وثيقًا ، وتستند النسخة من STL بقوة على سابقتها.فيما يلي التغييرات الرئيسية:والفرق الرئيسي هو أنه boost::any
لا يستخدم SBO ، لذلك يستهلك ذاكرة أقل بكثير (في GCC8.1 حجمه هو 8 بايت) ، ولكن لهذا السبب ، فإنه يخصص الذاكرة بشكل ديناميكي حتى للأنواع الصغيرة مثل int.أمثلة على استخدام std :: any
الميزة الرئيسية std::any
هي المرونة. في الأمثلة أدناه ، يمكنك رؤية بعض الأفكار (أو تطبيقات محددة) حيث std::any
يجعل استخدامه التطبيق أسهل قليلاً.تحليل الملف
في الأمثلة على std::variant
( يمكنك رؤيتها هنا [eng] ) يمكنك أن ترى كيف يمكنك تحليل ملفات التكوين وتخزين النتيجة في متغير نوع std::variant
. أنت الآن تكتب حلاً عامًا جدًا ، ربما يكون جزءًا من بعض المكتبات ، ثم قد لا تكون على دراية بجميع أنواع الأنواع المحتملة.من std::any
المرجح أن يكون تخزين البيانات باستخدام المعلمات جيدًا جدًا من حيث الأداء ، وفي الوقت نفسه يمنحك مرونة الحل.المراسلة
في Windows Api ، الذي تتم كتابته بشكل أساسي بلغة C ، هناك نظام مراسلة يستخدم معرف الرسالة مع معلمتين اختياريتين تخزن بيانات الرسالة. بناءً على هذه الآلية ، يمكنك تنفيذ WndProc ، الذي يعالج الرسالة المرسلة إلى نافذتك.LRESULT CALLBACK WindowProc(
_In_ HWND hwnd,
_In_ UINT uMsg,
_In_ WPARAM wParam,
_In_ LPARAM lParam
);
الحقيقة هي أن البيانات يتم تخزينها في wParam
أو lParam
في أشكال مختلفة. في بعض الأحيان تحتاج فقط إلى استخدام بضعة بايت wParam
.ماذا لو قمنا بتغيير هذا النظام حتى تتمكن الرسالة من تمرير أي شيء إلى طريقة المعالجة؟على سبيل المثال:class Message
{
public:
enum class Type
{
Init,
Closing,
ShowWindow,
DrawWindow
};
public:
explicit Message(Type type, std::any param) :
mType(type),
mParam(param)
{ }
explicit Message(Type type) :
mType(type)
{ }
Type mType;
std::any mParam;
};
class Window
{
public:
virtual void HandleMessage(const Message& msg) = 0;
};
على سبيل المثال ، يمكنك إرسال رسالة إلى النافذة:Message m(Message::Type::ShowWindow, std::make_pair(10, 11));
yourWindow.HandleMessage(m);
يمكن للنافذة الرد على رسالة مثل هذه:switch (msg.mType) {
case Message::Type::ShowWindow:
{
auto pos = std::any_cast<std::pair<int, int>>(msg.mParam);
std::cout << "ShowWidow: "
<< pos.first << ", "
<< pos.second << "\n";
break;
}
}
بالطبع ، عليك تحديد كيفية تخزين نوع البيانات في الرسائل ، ولكن الآن يمكنك استخدام أنواع حقيقية بدلاً من الحيل المختلفة مع الأرقام.الخصائص
يعرض المستند الأصلي الذي يمثله أي C ++ (N1939) مثالاً لكائن خاصية:struct property
{
property();
property(const std::string &, const std::any &);
std::string name;
std::any value;
};
typedef std::vector<property> properties;
يبدو هذا الكائن مفيدًا جدًا لأنه يمكنه تخزين العديد من الأنواع المختلفة. أول ما يتبادر إلى ذهني هو مثال على استخدامه في مدير واجهة المستخدم أو في محرر لعبة.نحن نعبر الحدود
في r / cpp كان هناك دفق حول std :: any. وكان هناك تعليق واحد عظيم على الأقل يلخص متى يجب استخدام النوع.من هذا التعليق :خلاصة القول هي أن std :: any يسمح لك بنقل الحقوق إلى البيانات العشوائية عبر الحدود التي لا تعرف نوعها.
كل ما تحدثت عنه من قبل قريب من هذه الفكرة:- في مكتبة الواجهة: أنت لا تعرف ما هي الأنواع التي يريد العميل استخدامها هناك
- المراسلة: نفس الفكرة - امنح العميل المرونة
- تحليل الملف: لدعم أي نوع
مجموع
في هذه المقالة ، تعلمنا الكثير عن std::any
!إليك بعض الأشياء التي يجب وضعها في الاعتبار:std::any
ليس فئة قالبstd::any
يستخدم أمثلية الكائنات الصغيرة ، لذلك لن يقوم بتخصيص ذاكرة ديناميكية للأنواع البسيطة مثل int أو double ، وبالنسبة للأنواع الأكبر ، سيتم استخدام ذاكرة إضافيةstd::any
يمكن تسميتها "ثقيلة" ، ولكنها توفر السلامة ومرونة أكبر- يمكن الحصول على الوصول إلى البيانات
std::any
بمساعدة any_cast
التي تقدم عدة "وسائط". على سبيل المثال ، في حالة حدوث خطأ ، قد يكون هناك استثناء أو مجرد إرجاع nullptr - استخدمها عندما لا تعرف بالضبط أنواع البيانات الممكنة ، وإلا فكر في استخدامها
std::variant