كل ما تحتاج لمعرفته حول std :: any

مرحبا يا هابر! نقدم انتباهكم إلى ترجمة مقالة "كل ما تحتاج إلى معرفته عن 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;
//   :

//    a  
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";
}

//    any  :
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::anystd::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); // int
std::any a3(MyType(10, 11));

// in_place:
std::any a4(std::in_place_type<MyType>, 10, 11);
std::any a5{std::in_place_type<std::string>, "Hello World"};

// make_any
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); // throw!
    }
    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، وإليك نتائج العديد من المترجمات:

المترجمحجم (أي)
GCC 8.1 (Coliru)السادس عشر
Clang 7.0.0 (Wandbox)32
MSVC 2017 15.7.0 32 بت40
MSVC 2017 15.7.0 64 بت64

بشكل عام ، كما ترى ، std::anyهذا ليس نوعًا بسيطًا ، ويجلب تكاليف إضافية. عادة ما يستغرق الكثير من الذاكرة ، بسبب SBO ، من 16 إلى 32 بايت (في GCC أو clang ... أو حتى 64 بايت في MSVC!).

الترحيل من التعزيز :: أي


boost::anyتم تقديمه في مكان ما في عام 2001 (الإصدار 1.23.0). بالإضافة إلى ذلك ، فإن المؤلف boost::any(Kevlin Henney) هو أيضًا صاحب الاقتراح std::any. لذلك ، يرتبط هذان النوعان ارتباطًا وثيقًا ، وتستند النسخة من STL بقوة على سابقتها.

فيما يلي التغييرات الرئيسية:

وظيفةBoost.Any
(1.67.0)
الأمراض المنقولة جنسيا :: أي
تخصيص ذاكرة إضافيةنعمنعم
تحسين الأشياء الصغيرةلانعم
emplaceلانعم
in_place_type_t في المُنشئلانعم


والفرق الرئيسي هو أنه 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

All Articles