يا هذا std :: make_shared ...

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



وإذا كنت تحفر أعمق قليلا؟



std :: make_shared مفيدة



لماذا ظهر std :: make_shared في STL على الإطلاق؟



هناك مثال متعارف عليه حيث يمكن أن يؤدي إنشاء std :: Shared_ptr من مؤشر خام تم إنشاؤه حديثًا إلى تسرب للذاكرة:



process(std::shared_ptr<Bar>(new Bar), foo());


لحساب وسيطات دالة العملية (...) ، يجب استدعاء:



  • شريط جديد ؛
  • منشئ STD :: Shared_ptr ؛
  • foo ().


يمكن للمترجم مزجها بأي ترتيب ، على سبيل المثال مثل هذا:



  • شريط جديد ؛
  • foo () ؛
  • منشئ الأمراض المنقولة جنسيا :: Shared_ptr.


إذا حدث استثناء في foo () ، نحصل على تسرب لمثيل Bar.



لا يحتوي أي من أمثلة التعليمات البرمجية التالية على تسرب محتمل (ولكننا سنعود إلى هذا السؤال):



auto bar = std::shared_ptr<Bar>(new Bar);


auto bar = std::shared_ptr<Bar>(new Bar);
process(bar, foo());


process(std::shared_ptr<Bar>(new Bar));


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



ولتحقيق تسرب محتمل للذاكرة ، يلزم توفر شرطين إضافيين:



  • بحيث يمزج المترجم المكالمات بطريقة غير مواتية ؛
  • بحيث أن دالة تقييم المعلمة الثانية تطرح استثناء.


من غير المحتمل أن يحدث هذا الرمز الخطير أكثر من مرة في مائة استخدام لـ std :: Shared_ptr.

وللتعويض عن هذا الخطر ، تم دعم std :: Shared_ptr بواسطة عكاز يسمى std :: make_shared.



لتحلية الحبة قليلاً ، تمت إضافة العبارة التالية إلى وصف std :: make_shared في المعيار:

ملاحظات: يجب ألا تؤدي عمليات التنفيذ أكثر من تخصيص واحد للذاكرة.


ملاحظة: يجب ألا تقوم عمليات التنفيذ بتخصيص أكثر من ذاكرة واحدة.



لا ، هذا ليس ضمانًا.

ولكن يقول cppreference أن جميع عمليات التنفيذ المعروفة تفعل ذلك بالضبط.



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



الأمراض المنقولة جنسيا :: make_shared عديمة الفائدة



بدءًا من c ++ 17 ، لم يعد من الممكن حدوث تسرب للذاكرة في ذلك المثال النادر الصعب الذي تمت إضافة std :: make_shared إليه.



روابط الدراسة:





هناك عدة حالات أخرى يكون فيها std :: make_shared عديم الفائدة:



لن يتمكن std :: make_shared من الاتصال بالمنشئ الخاص
#include <memory>

class Bar
{
public:
    static std::shared_ptr<Bar> create()
    {
        // return std::make_shared<Bar>(); - no build
        return std::shared_ptr<Bar>(new Bar);
    }

private:
    Bar() = default;
};

int main()
{
    auto bar = Bar::create();

    return 0;
}




لا يدعم std :: make_shared المحددات المخصصة
… variadic template. , , deleter.

std::make_shared_with_custom_deleter…



من الجيد أن تعرف على الأقل هذه المشاكل في وقت الترجمة ...



الأمراض المنقولة جنسيا :: make_shared ضارة



نحن نمر في وقت التشغيل.



سيتم تجاهل عامل التشغيل الزائد الجديد وحذف عامل التشغيل بواسطة std :: make_shared
#include <memory>
#include <iostream>

class Bar
{
public:
    void* operator new(size_t)
    {
        std::cout << __func__ << std::endl;
        return ::new Bar();
    }

    void operator delete(void* bar)
    {
        std::cout << __func__ << std::endl;
        ::delete static_cast<Bar*>(bar);
    }
};

int main()
{
    auto bar = std::shared_ptr<Bar>(new Bar);
    // auto bar = std::make_shared<Bar>();

    return 0;
}


std::shared_ptr:

operator new

operator delete



std::make_shared:





والآن - أهم شيء بدأ المقال نفسه من أجله.



والمثير للدهشة أن هذا صحيح: كيف ستتعامل std :: Shared_ptr مع الذاكرة يمكن أن تعتمد بشكل كبير على كيفية إنشائها - باستخدام std :: make_shared أو استخدام مُنشئ!



لماذا يحدث هذا؟



لأن التخصيص الموحد "المفيد" الناتج عن std :: make_shared له تأثير جانبي متأصل في الاتصال غير الضروري بين كتلة التحكم والكائن المُدار. ببساطة لا يمكن الإفراج عنهم بشكل فردي. يجب أن تعيش كتلة التحكم طالما هناك ارتباط ضعيف واحد على الأقل.



يجب أن تتوقع الأداة std :: Shared_ptr التي تم إنشاؤها باستخدام مُنشئ السلوك التالي:



  • تخصيص كائن مُدار (قبل استدعاء المُنشئ ، أي من جانب المستخدم) ؛
  • تخصيص وحدة التحكم ؛
  • عند تدمير آخر مرجع قوي - استدعاء مدمر الكائن المُدار وتحرير الذاكرة التي تحتلها ؛ إذا لم يكن هناك ارتباط ضعيف واحد ، فقم بتحرير وحدة التحكم ؛
  • على تدمير الحلقة الأخيرة الضعيفة في غياب روابط قوية - إطلاق كتلة التحكم.


وإذا تم إنشاؤه باستخدام std :: make_shared:



  • تخصيص الكائن المُدار ووحدة التحكم ؛
  • عند تدمير آخر رابط قوي ، استدعاء مدمر للكائن المدار دون تحرير الذاكرة التي يشغلها ؛ إذا لم يكن هناك ارتباط ضعيف واحد ، فقم بتحرير كتلة التحكم وذاكرة الكائن المُدار ؛
  • — .


إنشاء std :: Shared_ptr باستخدام std :: make_shared يثير تسرب مساحة.



من المستحيل التمييز في وقت التشغيل بالضبط كيف تم إنشاء مثيل std :: Shared_ptr.



دعنا ننتقل إلى اختبار هذا السلوك.



هناك طريقة بسيطة للغاية - استخدم std :: signatureate_shared مع أداة تخصيص مخصصة ، والتي ستبلغ عن جميع المكالمات إليها. ولكن من الخطأ توزيع النتائج التي تم الحصول عليها بهذه الطريقة على std :: make_shared.



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



كود لينكس المختبر على Ubuntu 20.04 x64 المكتبي معطى. من يرغب في تكرار هذا لمنصات أخرى - انظر هنا (أظهرت تجربتي مع macOs أن خيار TASK_BASIC_INFO لا يتتبع إصدار الذاكرة ، وأن TASK_VM_INFO_PURGEABLE هو المرشح الأفضل).



المراقبة. ح
#pragma once

#include <cstdint>

uint64_t memUsage();




المراقبة. cpp
#include "Monitoring.h"

#include <fstream>
#include <string>

uint64_t memUsage()
{
    auto file = std::ifstream("/proc/self/status", std::ios_base::in);
    auto line = std::string();

    while(std::getline(file, line)) {
        if (line.find("VmSize") != std::string::npos) {
            std::string toConvert;
            for (const auto& elem : line) {
                if (std::isdigit(elem)) {
                    toConvert += elem;
                }
            }
            return stoull(toConvert);
        }
    }

    return 0;
}




main.cpp
#include <iostream>
#include <array>
#include <numeric>
#include <memory>

#include "Monitoring.h"

struct Big
{
    ~Big()
    {
        std::cout << __func__ << std::endl;
    }

    std::array<volatile unsigned char, 64*1024*1024> _data;
};

volatile uint64_t accumulator = 0;

int main()
{
    std::cout << "initial: " << memUsage() << std::endl;

    auto strong = std::shared_ptr<Big>(new Big);
    // auto strong = std::make_shared<Big>();

    std::accumulate(strong->_data.cbegin(), strong->_data.cend(), accumulator);

    auto weak = std::weak_ptr<Big>(strong);

    std::cout << "before reset: " << memUsage() << std::endl;

    strong.reset();

    std::cout << "after strong reset: " << memUsage() << std::endl;

    weak.reset();

    std::cout << "after weak reset: " << memUsage() << std::endl;

    return 0;
}




إخراج وحدة التحكم عند استخدام مُنشئ std :: Shared_ptr:

الأولي: 5884

قبل إعادة الضبط: 71424

~ كبير

بعد إعادة الضبط القوي: 5884

بعد إعادة الضبط الضعيفة: 5884



إخراج وحدة التحكم عند استخدام std :: make_shared:

الأولي: 5888

قبل إعادة الضبط: 71428

~ كبير

بعد إعادة الضبط القوي: 71428

بعد إعادة الضبط الضعيفة: 5888



علاوة



ومع ذلك ، هل من الممكن حدوث تسرب للذاكرة نتيجة لتنفيذ التعليمات البرمجية



auto bar = std::shared_ptr<Bar>(new Bar);


؟؟؟

ماذا يحدث إذا نجح تخصيص الشريط ، ولكن لا توجد ذاكرة كافية لكتلة التحكم؟



ماذا يحدث إذا تم استدعاء المُنشئ باستخدام مُحدد مخصص؟



يضمن المقطع [util.smartptr.shared.const] من المعيار أنه عند حدوث استثناء داخل مُنشئ std :: shared_ptr:



  • بالنسبة لمنشئ بدون delet مخصص ، سيتم حذف المؤشر الذي تم تمريره باستخدام حذف أو حذف [] ؛
  • بالنسبة لمنشئ مع deleter مخصص ، سيتم حذف المؤشر الذي تم تمريره باستخدام نفس deleter.


لا يوجد تسرب مضمون بالمعيار.



نتيجة لقراءة سريعة للتطبيقات في ثلاثة مترجمين (Apple clang الإصدار 11.0.3 ، GCC 9.3.0 ، MSVC 2019 16.6.2) ، يمكنني أن أؤكد أن كل شيء على ما يرام.



انتاج |



في C ++ 11 و C ++ 14 ، يمكن موازنة الضرر الناتج عن استخدام std :: make_shared من خلال وظيفته المفيدة الوحيدة.



نظرًا لأن c ++ 17 ، فإن الحساب ليس لصالح std :: make_shared على الإطلاق.



الموقف مشابه مع std :: customate_shared.



ينطبق الكثير مما سبق أيضًا على std :: make_unique ، ولكن الضرر أقل منه.



All Articles