مقدمة
تستخدم التطبيقات الكبيرة التكوينات لنقل الإعدادات. وغالبًا ما يحدث أن تحرير الميزات وحذفها يؤدي إلى عدم التزامن بين رمز التطبيق وما يتم تخزينه في هذه الإعدادات نفسها. ببساطة ، في البيانات الأخيرة التي لن تستخدمها مرة أخرى تسوية. مثل هذه الإعدادات ، من الناحية المثالية ، أرغب في التتبع ووضع علامة على أنها مهملة أو حذفها تمامًا.
المشكلة
لقد حدث أن معظم التهيئة داخل مشروعنا تمت كتابتها في خليط من json و yaml وتحليلها باستخدام مكتبة libconfig . لم تكن هناك رغبة في إعادة كتابة الكود المقابل ومحتويات التكوينات ، على سبيل المثال ، على yaml ، خاصةً عندما تكون هناك مجموعة من المهام الأخرى الأكثر تشويقًا وتعقيدًا. وجزء المكتبة المكتوب بلغة C جيد في حد ذاته: إنه مستقر وغني بالوظائف (في المجمع في C ++ ، كل شيء ليس بهذه البساطة).
في أحد الأيام الجميلة ، حرصنا على معرفة مقدار القمامة التي جمعناها في ملفات التكوين. لسوء الحظ ، لم يكن لدى libconfig مثل هذا الخيار. أولاً ، حاولنا تفرع المشروع على github وإجراء تغييرات على الجزء الذي تمت كتابته في C ++ (جميع أنواع أساليب البحث والمشغل []) ، وأتمتة عملية تعيين العلامة التي تمت زيارتها للعقدة. ولكن هذا من شأنه أن يؤدي إلى رقعة كبيرة للغاية ، وربما يتم تأخير اعتماده. ثم وقع الاختيار في اتجاه كتابة برنامج التضمين الخاص بك في C ++ ، دون التأثير على لب libconfig.
من وجهة نظر استخدام الإخراج ، لدينا ما يلي:
#include <variti/util/config.hpp> #include <iostream> #include <cassert> int main(int argc, char* argv[]) { using namespace variti; using namespace variti::util; assert(argc = 2); config conf( [](const config_setting& st) { if (!st.visited()) std::cerr << "config not visited: " << st.path() << "\n"; }); conf.load(argv[1]); auto root = conf.root(); root["module"]["name"].to_string(); return 0; }
laptop :: work/configpp/example ‹master*› % cat config_example1.conf version = "1.0"; module: { name = "module1"; submodules = ( { name = "submodule1"; }, { name = "submodule2"; } ); }; laptop :: work/configpp/example ‹master*› % ./config-example1 config_example1.conf config not visited: root.module.submodules.0.name config not visited: root.module.submodules.1.name
في رمز المثال ، تحولنا إلى إعداد module.name. لم يتم الوصول إلى الإعدادات module.submodules.0.name و module.submodules.1.name. هذا هو ما أبلغنا في السجل.
اختام
كيفية تنفيذ هذا إذا كانت العلامة التي تمت زيارتها أو شيء من هذا القبيل ليس داخل libconfig؟ فكر مطورو المكتبة مسبقًا وأضفوا القدرة على ربط خطاف إلى عقدة config_setting_t ، والتي تم تعيينها باستخدام دالة config_setting_set_hook وقراءتها باستخدام config_setting_get_hook.
حدد هذا الخطاف بأنه:
struct config_setting_hook { bool visited{false}; };
يوجد بنيتان رئيسيتان داخل libconfig: config_t و config_setting_t. يوفر الأول الوصول إلى التكوين بأكمله ككل وإرجاع مؤشر إلى عقدة الجذر config_setting_t ، والثاني - الوصول إلى العقد الأصل والتابعة ، وكذلك القيمة داخل العقدة الحالية.
نحن التفاف كل الهياكل في الفئات المقابلة - مقابض.
التعامل مع config_t:
using config_notify = std::function<void(const config_setting&)>; struct config : boost::noncopyable { config(config_notify n = nullptr); ~config(); void load(const std::string& filename); config_setting root() const; config_notify n; config_t* h; };
لاحظ أنه يتم تمرير وظيفة إلى مُنشئ التهيئة الذي سيتم استدعاؤه في destructor في وقت اجتياز جميع العقد المتطرفة. كيف يمكن استخدامها يمكن أن ينظر إليه في المثال أعلاه.
تعامل مع config_setting_t:
struct config_setting : boost::noncopyable { config_setting(config_setting_t* h, bool visit = false); ~config_setting(); bool to_bool() const; std::int32_t to_int32() const; std::int64_t to_int64() const; double to_double() const; std::string to_string() const; bool is_bool() const; bool is_int32() const; bool is_int64() const; bool is_double() const; bool is_string() const; bool is_group() const; bool is_array() const; bool is_list() const; bool is_scalar() const; bool is_root() const; std::string path() const; std::size_t size() const; bool exists(const std::string& name) const; config_setting parent() const; config_setting lookup(const std::string& name, bool visit = false) const; config_setting lookup(std::size_t indx, bool visit = false) const; config_setting operator[](const std::string& name) const; config_setting operator[](std::size_t indx) const; std::string filename() const; std::size_t fileline() const; bool visited() const; config_setting_t* h; };
يكمن السحر الرئيسي في طرق البحث. من المفترض أن يتم تعيين علامة العقد التي تمت زيارتها خلال الوسيطة الأخيرة التي تسمى الزيارة ، والتي تكون خاطئة بشكل افتراضي. لديك الحق في الإشارة إلى هذه القيمة بنفسك. ولكن نظرًا لأن الوصول الأكثر شيوعًا إلى العقد لا يزال عبر المشغل [] ، يتم استدعاء طريقة البحث بداخله مع زيارة تساوي true. وبالتالي ، سيتم تلقائيًا تمييز العقد التي تستدعي عامل التشغيل [] بأنها تمت زيارتها. علاوة على ذلك ، عند الزيارة ، سيتم تمييز سلسلة العقد بالكامل من الحالية إلى الجذر.
دعنا ننتقل إلى التنفيذ. نعرضها تمامًا لفئة التكوين:
config::config(config_notify n) : n(n) { h = (config_t*)malloc(sizeof(config_t)); config_init(h); config_set_destructor(h, [](void* p) { delete reinterpret_cast<config_setting_hook*>(p); }); } config::~config() { if (n) for_each(root(), n); config_destroy(h); free(h); } void config::load(const std::string& filename) { if (!config_read_file(h, filename.c_str())) throw std::runtime_error(std::string("config read file error: ") + filename); } config_setting config::root() const { return config_setting(config_root_setting(h)); }
وجزئياً من أجل config_setting:
config_setting::config_setting(config_setting_t* h, bool visit) : h(h) { assert(h); if (!config_setting_get_hook(h)) hook(h, new config_setting_hook()) if (visit) visit_up(h); } config_setting::~config_setting() { h = nullptr; } std::size_t config_setting::size() const { return config_setting_length(h); } config_setting config_setting::parent() const { return config_setting(config_setting_parent(h)); } bool config_setting::exists(const std::string& name) const { if (!is_group()) return false; return config_setting_get_member(h, name.c_str()); } config_setting config_setting::lookup(const std::string& name, bool visit) const { assert(is_group()); auto p = config_setting_get_member(h, name.c_str()); if (!p) throw_not_found(*this); return config_setting(p, visit); } config_setting config_setting::lookup(std::size_t indx, bool visit) const { assert(is_group() || is_array() || is_list()); auto p = config_setting_get_elem(h, indx); if (!p) throw_not_found(*this); return config_setting(p, visit); } config_setting config_setting::operator[](const std::string& name) const { return lookup(name, true); } config_setting config_setting::operator[](std::size_t indx) const { return lookup(indx, true); } bool config_setting::visited() const { return boost::algorithm::starts_with(path(), "root") || boost::algorithm::starts_with(path(), "root.version") || hook(h)->visited; }
سننظر بشكل منفصل في المساعدين للعمل مع خطاف:
void hook(config_setting_t* h, config_setting_hook* k) { config_setting_set_hook(h, k); } config_setting_hook* hook(config_setting_t* h) { return reinterpret_cast<config_setting_hook*>(config_setting_get_hook(h)); } void visit_up(config_setting_t* h) { for (; !config_setting_is_root(h) && !hook(h)->visited; h = config_setting_parent(h)) hook(h)->visited = true; }
ومساعد لتجاوز العقد القصوى:
template <typename F> void for_each(const config_setting& st, F f) { if (st.size()) for (std::size_t i = 0; i < st.size(); ++i) for_each(st.lookup(i), f); else f(st); }
استنتاج
لقد تحولت إلى رمز جميل وأكثر مرونة في رأينا. لكننا لم نتخلى عن فكرة إجراء تغييرات مماثلة لمكتبة libconfig الأصلية ، أو بالأحرى ، واجهتها المكتوبة بلغة C ++. يتم الآن إعداد طلب سحب ، لكننا نعمل بالفعل ونقوم بتنظيف الإعدادات من الإعدادات غير المستخدمة.
تطبيق
تحقق من شفرة المصدر هنا !