C ++ 17 مخصصات متعددة الأشكال

قريبا جدا ، تيار جديد من الدورة التدريبية “C ++ Developer. محترف " . عشية بدء الدورة ، أعد خبيرنا ألكسندر كليوتشيف مادة شيقة حول المخصصات متعددة الأشكال. نعطي الكلمة للكسندر:










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



الفكرة الرئيسية للمخصصات متعددة الأشكال التي تم تقديمها في c ++ 17 هي تحسين المخصصات المعيارية المطبقة على أساس تعدد الأشكال الثابت أو بعبارة أخرى القوالب. إنها أسهل في الاستخدام من المخصصات القياسية ، بالإضافة إلى أنها تسمح لك بالحفاظ على نوع الحاوية عند استخدام مُخصصات مختلفة ، وبالتالي تغيير المُخصصات في وقت التشغيل.



إذا كنت تريد std::vectorمع مخصص ذاكرة معين ، يمكنك استخدام معلمة قالب المخصص:



auto my_vector = std::vector<int, my_allocator>();




ولكن هناك مشكلة - هذا المتجه ليس من نفس نوع المتجه بمخصص مختلف ، بما في ذلك المحدد بشكل افتراضي.

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



auto my_vector = std::vector<int, my_allocator>();
auto my_vector2 = std::vector<int, other_allocator>();
auto vec = my_vector; // ok
vec = my_vector2; // error


يحتوي المخصص متعدد الأشكال على مؤشر إلى واجهة memory_resourceبحيث يمكنه استخدام الإرسال الديناميكي.



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



توجد أنواع البيانات المحددة التي يستخدمها المُخصص الجديد في مساحة الاسم std::pmr. هناك أيضًا تخصصات نموذجية للحاويات القياسية التي يمكن أن تعمل مع مُخصص متعدد الأشكال.



تتمثل إحدى المشكلات الرئيسية في الوقت الحالي في عدم توافق الإصدارات الجديدة من الحاويات std::pmrمع نظائرها من std.



المكونات الرئيسية std::pmr:



  • std::pmr::memory_resource — , .
  • :

    • virtual void* do_allocate(std::size_t bytes, std::size_t alignment),
    • virtual void do_deallocate(void* p, std::size_t bytes, std::size_t alignment)
    • virtual bool do_is_equal(const std::pmr::memory_resource& other) const noexcept.
  • std::pmr::polymorphic_allocator — , memory_resource .
  • new_delete_resource() null_memory_resource() «»
  • :

    • synchronized_pool_resource
    • unsynchronized_pool_resource
    • monotonic_buffer_resource
  • , std::pmr::vector, std::pmr::string, std::pmr::map . , .
  • memory_resource:

    • memory_resource* new_delete_resource() , memory_resource, new delete .
    • memory_resource* null_memory_resource()

      تُرجع الدالة free مؤشرًا memory_resourceيطرح استثناءًا std::bad_allocعند كل محاولة تخصيص.

      يمكن أن يكون هذا مفيدًا للتأكد من أن الكائنات لا تخصص ذاكرة على الكومة أو لأغراض الاختبار.




  • class synchronized_pool_resource : public std::pmr::memory_resource

    يتكون تنفيذ memory_resource للأغراض العامة وآمن بخيط التنفيذ من مجموعة من المجمعات ذات أحجام مختلفة من كتل الذاكرة.

    كل تجمع عبارة عن مجموعة من أجزاء الذاكرة من نفس الحجم.
  • class unsynchronized_pool_resource : public std::pmr::memory_resource

    نسخة مترابطة واحدة synchronized_pool_resource.
  • class monotonic_buffer_resource : public std::pmr::memory_resource

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


مثال على الاستخدام monotonic_buffer_resourceو pmr::vector:



#include <iostream>
#include <memory_resource>   // pmr core types
#include <vector>        	// pmr::vector
#include <string>        	// pmr::string
 
int main() {
	char buffer[64] = {}; // a small buffer on the stack
	std::fill_n(std::begin(buffer), std::size(buffer) - 1, '_');
	std::cout << buffer << '\n';
 
	std::pmr::monotonic_buffer_resource pool{std::data(buffer), std::size(buffer)};
 
	std::pmr::vector<char> vec{ &pool };
	for (char ch = 'a'; ch <= 'z'; ++ch)
    	vec.push_back(ch);
 
	std::cout << buffer << '\n';
}


مخرجات البرنامج:




_______________________________________________________________
aababcdabcdefghabcdefghijklmnopabcdefghijklmnopqrstuvwxyz______


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



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



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



تخزين pmr::string



ماذا لو أردنا تخزين الخيوط فيها pmr::vector؟



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



إذا كنت ترغب في الاستفادة من هذه الميزة ، فيجب عليك استخدامها std::pmr::stringبدلاً من ذلك std::string.



النظر في المثال مع العازلة مسبقا المخصصة على المكدس، ونحن سوف تمر كما memory_resourceل std::pmr::vector std::pmr::string:



#include <iostream>
#include <memory_resource>   // pmr core types
#include <vector>        	// pmr::vector
#include <string>        	// pmr::string
 
int main() {
	std::cout << "sizeof(std::string): " << sizeof(std::string) << '\n';
	std::cout << "sizeof(std::pmr::string): " << sizeof(std::pmr::string) << '\n';
 
	char buffer[256] = {}; // a small buffer on the stack
	std::fill_n(std::begin(buffer), std::size(buffer) - 1, '_');
 
	const auto BufferPrinter = [](std::string_view buf, std::string_view title) {
    	std::cout << title << ":\n";
    	for (auto& ch : buf) {
        	std::cout << (ch >= ' ' ? ch : '#');
    	}
    	std::cout << '\n';
	};
 
	BufferPrinter(buffer, "zeroed buffer");
 
	std::pmr::monotonic_buffer_resource pool{std::data(buffer), std::size(buffer)};
	std::pmr::vector<std::pmr::string> vec{ &pool };
	vec.reserve(5);
 
	vec.push_back("Hello World");
	vec.push_back("One Two Three");
	BufferPrinter(std::string_view(buffer, std::size(buffer)), "after two short strings");
 
	vec.emplace_back("This is a longer string");
	BufferPrinter(std::string_view(buffer, std::size(buffer)), "after longer string strings");
 
	vec.push_back("Four Five Six");
	BufferPrinter(std::string_view(buffer, std::size(buffer)), "after the last string");   
}


مخرجات البرنامج:



sizeof(std::string): 32
sizeof(std::pmr::string): 40
zeroed buffer:
_______________________________________________________________________________________________________________________________________________________________________________________________________________________________________________________________
after two short strings:
#m### ###n### ##########Hello World######m### ##@n### ##########One Two Three###_______________________________________________________________________________________________________________________________________________________________________________#
after longer string strings:
#m### ###n### ##########Hello World######m### ##@n### ##########One Two Three####m### ###n### ##################________________________________________________________________________________________This is a longer string#_______________________________#
after the last string:
#m### ###n### ##########Hello World######m### ##@n### ##########One Two Three####m### ###n### ##################________#m### ###n### ##########Four Five Six###________________________________________This is a longer string#_______________________________#


النقاط الرئيسية التي يجب الانتباه إليها في هذا المثال:



  • الحجم pmr::stringأكبر من std::string. هذا يرجع إلى حقيقة أن المؤشر إلى memory_resource؛
  • نحتفظ بالمتجه لـ 5 عناصر ، لذلك لا تحدث عمليات إعادة تخصيص عند إضافة 4.
  • أول سطرين قصيران بما يكفي لكتلة ذاكرة المتجه ، لذلك لا يحدث تخصيص إضافي للذاكرة.
  • السطر الثالث أطول ويتطلب جزءًا منفصلًا من الذاكرة داخل المخزن المؤقت لدينا ، ويتم تخزين المؤشر إلى هذه الكتلة فقط في المتجه.
  • كما ترى من الإخراج ، "هذه سلسلة أطول" تقع تقريبًا في نهاية المخزن المؤقت.
  • عندما نقوم بإدخال سلسلة قصيرة أخرى ، فإنها تعود إلى كتلة ذاكرة المتجه


للمقارنة ، دعنا نجري نفس التجربة std::stringبدلاً منstd::pmr::string



sizeof(std::string): 32
sizeof(std::pmr::string): 40
zeroed buffer:
_______________________________________________________________________________________________________________________________________________________________________________________________________________________________________________________________
after two short strings:
###w# ##########Hello World########w# ##########One Two Three###_______________________________________________________________________________________________________________________________________________________________________________________________#
new 24
after longer string strings:
###w# ##########Hello World########w# ##########One Two Three###0#######################_______________________________________________________________________________________________________________________________________________________________________#
after the last string:
###w# ##########Hello World########w# ##########One Two Three###0#######################________@##w# ##########Four Five Six###_______________________________________________________________________________________________________________________________#




هذه المرة ، تشغل العناصر الموجودة في الحاوية مساحة أقل لأنه لا توجد حاجة لتخزين مؤشر في memory_resource.

لا تزال السلاسل القصيرة مخزنة داخل كتلة ذاكرة المتجه ، ولكن الآن السلسلة الطويلة لا تجعلها في المخزن المؤقت لدينا. هذه المرة يتم تخصيص سلسلة طويلة باستخدام المخصص الافتراضي ويتم وضع

مؤشر لها في كتلة ذاكرة المتجه . لذلك ، لا نرى هذا الخط في الإخراج.



مرة أخرى حول توسيع المتجه:



تم ذكر أنه عندما تنفد الذاكرة في التجمع ، يطلبها المخصص باستخدام عامل التشغيل new().



في الواقع ، هذا ليس صحيحًا تمامًا - يتم طلب الذاكرة من memory_resource، وإعادتها باستخدام وظيفة مجانية

std::pmr::memory_resource* get_default_resource()

افتراضيًا ، تعود هذه الوظيفة

std::pmr::new_delete_resource()، والتي بدورها تخصص الذاكرة باستخدام عامل تشغيل new()، ولكن يمكن استبدالها باستخدام وظيفة

std::pmr::memory_resource* set_default_resource(std::pmr::memory_resource* r)



، لذا ، دعونا ننظر إلى مثال عندما get_default_resourceتُرجع قيمة بواسطة إفتراضي.



وينبغي أن يوضع في الاعتبار أن الأساليب do_allocate()و do_deallocate()استخدام "المحاذاة" حجة، لذلك نحن بحاجة إلى C ++ 17 نسخة new()مع دعم التوافق:



void* lastAllocatedPtr = nullptr;
size_t lastSize = 0;
 
void* operator new(std::size_t size, std::align_val_t align) {
#if defined(_WIN32) || defined(__CYGWIN__)
	auto ptr = _aligned_malloc(size, static_cast<std::size_t>(align));
#else
	auto ptr = aligned_alloc(static_cast<std::size_t>(align), size);
#endif
 
	if (!ptr)
    	throw std::bad_alloc{};
 
	std::cout << "new: " << size << ", align: "
          	<< static_cast<std::size_t>(align)
  	        << ", ptr: " << ptr << '\n';
 
	lastAllocatedPtr = ptr;
	lastSize = size;
 
	return ptr;
}


دعنا الآن نعود إلى النظر إلى المثال الرئيسي:



constexpr auto buf_size = 32;
uint16_t buffer[buf_size] = {}; // a small buffer on the stack
std::fill_n(std::begin(buffer), std::size(buffer) - 1, 0);
 
std::pmr::monotonic_buffer_resource pool{std::data(buffer), std::size(buffer)*sizeof(uint16_t)};
 
std::pmr::vector<uint16_t> vec{ &pool };
 
for (int i = 1; i <= 20; ++i)
	vec.push_back(i);
 
for (int i = 0; i < buf_size; ++i)
	std::cout <<  buffer[i] << " ";
 
std::cout << std::endl;
 
auto* bufTemp = (uint16_t *)lastAllocatedPtr;
 
for (unsigned i = 0; i < lastSize; ++i)
	std::cout << bufTemp[i] << " ";


يحاول البرنامج وضع 20 رقمًا في متجه ، ولكن نظرًا لأن المتجه ينمو فقط ، نحتاج إلى مساحة أكبر من المخزن المؤقت المحجوز بـ 32 إدخالًا.



لذلك ، في مرحلة ما ، سيطلب المخصص الذاكرة من get_default_resourceخلالها ، والتي بدورها ستؤدي إلى استدعاء عالمي new().



مخرجات البرنامج:



new: 128, align: 16, ptr: 0xc73b20
1 1 2 1 2 3 4 1 2 3 4 5 6 7 8 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 0
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 65535 65535 65535 65535 65535 65535 65535 65535 65535 65535 65535 65535 65535 65535 65535 65535 65535 65535 65535 65535 65535 65535 65535 65535 65535 65535 65535 65535 65535 65535 65535 65535 65535 65535 65535 65535 132 0 0 0 0 0 0 0 144 0 0 0 65 0 0 0 16080 199 0 0 16176 199 0 0 16176 199 0 0 15344 199 0 0 15472 199 0 0 15472 199 0 0 0 0 0 0 145 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0


إذا حكمنا من خلال الإخراج إلى وحدة التحكم ، فإن المخزن المؤقت المخصص يكفي لـ 16 عنصرًا فقط ، وعندما نقوم بإدخال الرقم 17 ، يحدث تخصيص جديد لـ 128 بايت باستخدام المشغل new().



في السطر الثالث ، نرى كتلة من الذاكرة مخصصة باستخدام عامل التشغيل new().



من غير new()المحتمل أن يكون المثال أعلاه مع تجاوز عامل التشغيل مناسبًا لحل منتج.



لحسن الحظ ، لا أحد يزعجنا في تنفيذ واجهتنا الخاصة memory_resource.



كل ما نحتاجه هو



  • ترث من std::pmr::memory_resource
  • تنفيذ الطرق:

    • do_allocate()
    • do_deallocate()
    • do_is_equal()
  • مرر تنفيذنا إلى memory_resourceالحاويات.


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





اقرأ أكثر






All Articles