![]() |
![]() ![]() ![]() ![]() ![]() |
![]() |
AttributesBoost , Chapter 1. Boost.Log v2 , Detailed features description
|
![]() | Note |
---|---|
Не ожидайте, что записи журнала с атрибутом |
#include <boost/log/attributes/clock.hpp
>
Одной из «обязательных» особенностей любой библиотеки журналов является поддержка прикрепления метки времени к каждой записи журнала. Библиотека предоставляет для этой цели два атрибута:utc_clock
иlocal_clock
. Первый возвращает текущее время UTC, а второй возвращает текущее местное время. В любом случае возвращаемая временная метка приобретается с максимальной точностью для целевой платформы. Значение атрибутаповышает::posix_time::ptime
(см.Boost.DateTime). Использование довольно простое:
BOOST_LOG_DECLARE_GLOBAL_LOGGER(my_logger, src::logger_mt) void foo() { logging::core::get()->add_global_attribute( "TimeStamp", attrs::local_clock()); // Now every log record ever made will have a time stamp attached src::logger_mt& lg = get_my_logger(); BOOST_LOG(lg) << "This record has a time stamp"; }
#include <boost/log/attributes/timer.hpp
>
Атрибуттаймера
очень полезен, когда есть необходимость оценить продолжительность какого-то длительного процесса. Атрибут возвращает время, прошедшее с момента построения атрибута. Тип значения атрибута —boost::posix_time::ptime::time_duration_type
(см.Boost.DateTime).
// The class represents a single peer-to-peer connection class network_connection { src::logger m_logger; public: network_connection() { m_logger.add_attribute("Duration", attrs::timer()); BOOST_LOG(m_logger) << "Connection established"; } ~network_connection() { // This log record will show the whole life time duration of the connection BOOST_LOG(m_logger) << "Connection closed"; } };
Атрибут обеспечивает высокое разрешение оценки времени и может даже использоваться в качестве простого инструмента профилирования производительности на месте.
![]() | Tip |
---|---|
Атрибут |
#include <boost/log/attributes/named_scope.hpp
> // Supporting headers #include <boost/log/support/exception.hpp
>
Библиотека журналов поддерживает отслеживание стека областей во время выполнения приложения. Этот стек может быть записан для входа в систему или использоваться для других нужд (например, для сохранения точной последовательности вызовов, которая привела к исключению при броске). Каждый элемент стека содержит следующую информацию (см. определение шаблона структурыnamed_scope_entry
):
__FILE__
макрорасширения. Как и название области, имя файладолжно быть постоянным строковым буквальным.__LINE__
макрорасширения.Стек области применения реализуется в качестве глобального хранилища, специфичного для потоков. Существует атрибутnamed_scope
, который позволяет подключать этот стек в лесозаготовительный трубопровод. Этот атрибут генерирует значение вложенного типаnamed_scope::scope_stack
, которое является экземпляром стека scope. Атрибут может быть зарегистрирован следующим образом:
logging::core::get()->add_global_attribute("Scope", attrs::named_scope());
Обратите внимание, что это совершенно правильно, чтобы зарегистрировать атрибут глобально, потому что стек области действия является потоковой локальной в любом случае. Это также косвенно добавит отслеживание объема ко всем потокам приложения, что часто именно то, что необходимо.
Теперь мы можем отметить области выполнения макросамиBOOST_LOG_FUNCTION
иBOOST_LOG_NAMED_SCOPE
(последний принимает название области в качестве аргумента). Эти макросы автоматически добавляют информацию о местоположении источника к каждой позиции области. Ниже приводится пример:
void foo(int n) { // Mark the scope of the function foo BOOST_LOG_FUNCTION(); switch (n) { case 0: { // Mark the current scope BOOST_LOG_NAMED_SCOPE("case 0"); BOOST_LOG(lg) << "Some log record"; bar(); // call some function } break; case 1: { // Mark the current scope BOOST_LOG_NAMED_SCOPE("case 1"); BOOST_LOG(lg) << "Some log record"; bar(); // call some function } break; default: { // Mark the current scope BOOST_LOG_NAMED_SCOPE("default"); BOOST_LOG(lg) << "Some log record"; bar(); // call some function } break; } }
После выполненияfoo
мы сможем увидеть в журнале, что функцияbar
была вызвана изfoo
и, точнее, из утверждения случая, которое соответствует значениюn
. Это может быть очень полезно при отслеживании тонких ошибок, которые появляются только тогда, когдабар
вызывается из определенного места (например, еслибар)
в данном конкретном месте принимаются недействительные аргументы.
![]() | Note |
---|---|
|
Другим хорошим вариантом использования является прикрепление информации стека области применения к исключению. С помощьюBoost.Exceptionможно:
void bar(int x) { BOOST_LOG_FUNCTION(); if (x < 0) { // Attach a copy of the current scope stack to the exception throw boost::enable_error_info(std::range_error("x must not be negative")) << logging::current_scope(); } } void foo() { BOOST_LOG_FUNCTION(); try { bar(-1); } catch (std::range_error& e) { // Acquire the scope stack from the exception object BOOST_LOG(lg) << "bar call failed: " << e.what() << ", scopes stack:\n" << *boost::get_error_info< logging::current_scope_info >(e); } }
![]() | Note |
---|---|
Для компиляции этого кода необходимо включить заголовок поддержкиBoost.Exception. |
![]() | Note |
---|---|
Мы не вводим в исключение атрибут |
#include <boost/log/attributes/current_process_id.hpp
>
Часто полезно знать идентификатор процесса, который создает журнал, особенно если журнал может в конечном итоге объединить выход различных процессов. Атрибутcurrent_process_id
представляет собой константу, которая форматируется в идентификатор текущего процесса. Тип значения атрибута может быть определен посредствомcurrent_process_id::value_type
typedef.
void foo() { logging::core::get()->add_global_attribute( "ProcessID", attrs::current_process_id()); }
#include <boost/log/attributes/current_process_name.hpp
>
current_process_name
производитstd::значения строки
с исполняемым названием текущего процесса.
![]() | Note |
---|---|
Этот атрибут не является универсально портативным, хотя Windows, Linux и OS X поддерживаются. Атрибут может работать и на других системах POSIX, но он не был протестирован. Если имя процесса не может быть получено, атрибут сгенерирует строку с идентификатором процесса. |
void foo() { logging::core::get()->add_global_attribute( "Process", attrs::current_process_name()); }
#include <boost/log/attributes/current_thread_id.hpp
>
Многопоточные сборки библиотеки также поддерживают атрибутcurrent_thread_id
с типом значенияcurrent_thread_id::value_type
. Атрибут будет генерировать значения, специфичные для вызывающей нити. Использование аналогично процессу id.
void foo() { logging::core::get()->add_global_attribute( "ThreadID", attrs::current_thread_id()); }
![]() | Tip |
---|---|
Возможно, вы заметили, что атрибут зарегистрирован по всему миру. Это не приведет к тому, что все потоки, имеющие тот же ThreadID в записях журналов, что и атрибут, всегда будут возвращать значение, специфичное для потока. Дополнительным преимуществом является то, что вам не нужно делать что-то в процедурах инициализации потока, чтобы иметь значение атрибута потока в записях журнала. |
#include <boost/log/attributes/function.hpp
>
Этот атрибут представляет собой простую обертку вокруг определяемого пользователем функционального объекта. Каждая попытка получить значение атрибута приводит к вызову объекта функции. Результат вызова возвращается в качестве значения атрибута (это означает, что функция не должна возвращатьпустоту
). Атрибут объекта функции может быть построен с помощью функции помощникаmake_function
, такой как:
void foo() { logging::core::get()->add_global_attribute("MyRandomAttr", attrs::make_function(&std::rand)); }
Также поддерживаются автоматически генерируемые функциональные объекты, подобные тем, которые определены вBoost.Bindили STL.
![]() | Note |
---|---|
Некоторые недостающие компиляторы могут не поддерживать |
#include <boost/log/attributes/attribute_name.hpp
>
Имена атрибутов представлены объектамиатрибут_имя
, которые используются в качестве ключей в ассоциативных контейнерах атрибутов, используемых библиотекой.атрибут_имя
объект может быть создан из строки, поэтому большую часть времени его использование прозрачно.
Имя не хранится как строка внутри.атрибут_имя
объект. Вместо этого генерируется универсальный идентификатор процесса, связанный с конкретным именем. Эта связь сохраняется до окончания процесса, поэтому каждый разатрибут_имя
объект создан для того же имени, он получает тот же идентификатор. Тем не менее, ассоциация не стабильна в разных версиях приложения.
![]() | Warning |
---|---|
Поскольку связь между именами строк и идентификаторами включает в себя некоторое государственное распределение, не рекомендуется использовать внешние или известные изменения строк для имен атрибутов. Даже если имя не используется в каких-либо записях журнала, ассоциация сохраняется в любом случае. Непрерывное построение |
Работа с идентификаторами намного эффективнее, чем со строками. Например, копирование не требует динамического распределения памяти, а операторы сравнения очень легкие. С другой стороны, при необходимости легко получить считываемое человеком имя атрибута для презентации.
Классатрибут_name
поддерживает пустое (неинициализированное) состояние при построении по умолчанию. В этом состоянии имя объекта не равно любому другому инициализируемому имени объекта. Имена атрибутов не должны передаваться в библиотеку, но могут быть полезны в некоторых контекстах (например, когда требуется отсроченная инициализация).
#include <boost/log/attributes/attribute_set.hpp
>
Набор атрибутов представляет собой неупорядоченный ассоциативный контейнер, который отображаетимена атрибутовнаатрибуты. Он используется врегистраторахирегистрационном ядредля хранения исходных, нитевидных и глобальных атрибутов. Интерфейс очень похож на ассоциативные контейнеры STL и описан в ссылке классаатрибут_set
.
#include <boost/log/attributes/attribute_value_set.hpp
>
Набор значений атрибутов представляет собой неупорядоченный ассоциативный контейнер, который отображаетимена атрибутовназначения атрибутов. Этот контейнер используется в журналахдля представления значений атрибутов. В отличие от обычных контейнеров,атрибут_значение_множество
не поддерживает удаление или изменение элементов после вставки. Это гарантирует, что значения атрибутов, которые участвовали в фильтрации, не исчезнут из записи журнала в середине обработки.
Кроме того, набор может быть построен из трехнаборов атрибутов, которые интерпретируются как наборы исходных, нитевидных и глобальных атрибутов. Конструктор принимает значения атрибутов из трех наборов атрибутов в единый набор значений атрибутов. После строительстваатрибут_значение_множество
считается находящимся в незамороженном состоянии. Это означает, что контейнер может содержать ссылки на элементы наборов атрибутов, используемых в качестве источника для построения набора значений. В этом состоянии ни наборы атрибутов, ни набор значений не должны каким-либо образом изменяться, поскольку это может привести к повреждению набора значений. Набор значений может использоваться для чтения в этом состоянии, его операции поиска будут выполняться как обычно. Набор значений может быть заморожен путем вызова методазамораживания
; набор больше не будет присоединен к исходным наборам атрибутов и будет доступен для дальнейших вставок после этого вызова. Библиотека гарантирует, что набор значений всегда замораживается, когда запись журнала возвращается из регистрационного ядра; наборнезамораживается во время фильтрации.
![]() | Tip |
---|---|
В незамороженном состоянии набор значений может не иметь всех значений атрибутов, полученных из атрибутов. Он будет получать только те значения, которые запрашиваются фильтрами. После замораживания контейнер имеет все значения атрибутов. Этот переход позволяет оптимизировать библиотеку так, чтобы значения атрибутов приобретались только при необходимости. |
Для получения дополнительной информации о интерфейсе контейнера, пожалуйста, обратитесь к ссылкеатрибут_value_set
.
Поскольку значения атрибутов не отображают сохраненное значение в интерфейсе, для получения сохраненного значения необходим API. Библиотека предоставляет два API для этой цели: посещение и извлечение ценности.
#include <boost/log/attributes/value_visitation_fwd.hpp
> #include <boost/log/attributes/value_visitation.hpp
>
Посещение ценности атрибута реализует шаблон дизайна посетителя, отсюда и название. Пользователь должен предоставить унарный функциональный объект (посетитель), на который будет ссылаться сохраненное значение атрибута. Абонент также должен предоставить ожидаемый тип или набор возможных типов сохраненного значения. Очевидно, что посетитель должен быть в состоянии получить аргумент ожидаемого типа. Посещение будет успешным только в том случае, если сохраненный тип соответствует ожиданиям.
Для того чтобы применить посетителя, следует вызвать функциюна значение атрибута. Давайте посмотрим пример:
// Our attribute value visitor struct print_visitor { typedef void result_type; result_type operator() (int val) const { std::cout << "Visited value is int: " << val << std::endl; } result_type operator() (std::string const& val) const { std::cout << "Visited value is string: " << val << std::endl; } }; void print_value(logging::attribute_value const& attr) { // Define the set of expected types of the stored value typedef boost::mpl::vector< int, std::string > types; // Apply our visitor logging::visitation_result result = logging::visit< types >(attr, print_visitor()); // Check the result if (result) std::cout << "Visitation succeeded" << std::endl; else std::cout << "Visitation failed" << std::endl; }
В этом примере мы печатаем сохраненное значение атрибута в нашемprint_visitor
. Мы ожидаем, что значение атрибута будет иметь либоint
, либоstd::строка
сохраненного типа; только в этом случае посетитель будет вызван и результат посещения будет положительным. В случае отказа классvisitation_result
предоставляет дополнительную информацию о причине отказа. Класс имеет метод под названиемкод
, который возвращает код ошибки посещения. Возможны следующие коды ошибок:
ok
- посещение успешно, посетитель был вызван; результат посещения является положительным, когда этот код используетсяvalue_not_found
- посещение не удалось, потому что запрошенное значение не было найдено; этот код используется, когда посещение применяется к записи журнала или набору значений атрибутов, а не к одному значениюvalue_has_invalid_type
- посещение не удалось, поскольку значение имеет тип, отличающийся от любого из ожидаемых типовПо умолчанию результат функции посетителя игнорируется, но его можно получить. Для этого следует использовать специальную оберткуsave_result
для посетителя; обертка сохранит полученное посетителем значение во внешнюю переменную, захваченную ссылкой. Результат посетителя инициализируется, когда возвращенныйрезультат посещения
является положительным. Смотрите следующий пример, где мы вычисляем хеш-значение на сохраненное значение.
struct hash_visitor { typedef std::size_t result_type; result_type operator() (int val) const { std::size_t h = val; h = (h << 15) + h; h ^= (h >> 6) + (h << 7); return h; } result_type operator() (std::string const& val) const { std::size_t h = 0; for (std::string::const_iterator it = val.begin(), end = val.end(); it != end; ++it) h += *it; h = (h << 15) + h; h ^= (h >> 6) + (h << 7); return h; } }; void hash_value(logging::attribute_value const& attr) { // Define the set of expected types of the stored value typedef boost::mpl::vector< int, std::string > types; // Apply our visitor std::size_t h = 0; logging::visitation_result result = logging::visit< types >(attr, logging::save_result(hash_visitor(), h)); // Check the result if (result) std::cout << "Visitation succeeded, hash value: " << h << std::endl; else std::cout << "Visitation failed" << std::endl; }
![]() | Tip |
---|---|
При отсутствии состояния по умолчанию для результата посетителя удобно использоватьBoost.Optionalдля обертывания возвращенного значения. |
Как уже упоминалось, посещение может также применяться к записям журналов и наборам значений атрибутов. Синтаксис одинаков, за исключением того, что имя атрибута также должно быть указано. Алгоритмпосещения
попытается найти значение атрибута по имени, а затем применить посетителя к найденному элементу.
void hash_value(logging::record_view const& rec, logging::attribute_name name) { // Define the set of expected types of the stored value typedef boost::mpl::vector< int, std::string > types; // Apply our visitor std::size_t h = 0; logging::visitation_result result = logging::visit< types >(name, rec, logging::save_result(hash_visitor(), h)); // Check the result if (result) std::cout << "Visitation succeeded, hash value: " << h << std::endl; else std::cout << "Visitation failed" << std::endl; }
Кроме того, для удобстваатрибут_значение
имеет способ под названиемпосещение
с тем же значением, что и свободная функция, применяемая к значению атрибута.
#include <boost/log/attributes/value_extraction_fwd.hpp
> #include <boost/log/attributes/value_extraction.hpp
>
API извлечения значений атрибутов позволяет получить ссылку на сохраненное значение. Для этого не требуется объект функции посетителя, но пользователь все равно должен предоставить ожидаемый тип или набор типов, которые могут иметь сохраненное значение.
void print_value(logging::attribute_value const& attr) { // Extract a reference to the stored value logging::value_ref< int > val = logging::extract< int >(attr); // Check the result if (val) std::cout << "Extraction succeeded: " << val.get() << std::endl; else std::cout << "Extraction failed" << std::endl; }
В этом примере мы ожидаем, что значение атрибута будет иметь сохраненный типint
. Функцияизвлечения
пытается извлечь ссылку на сохраненное значение и возвращает заполненныйзначение_ref
объект, если это удается.
Экстракция ценности может также использоваться с набором ожидаемых типов хранения. Следующий фрагмент кода демонстрирует это:
void print_value_multiple_types(logging::attribute_value const& attr) { // Define the set of expected types of the stored value typedef boost::mpl::vector< int, std::string > types; // Extract a reference to the stored value logging::value_ref< types > val = logging::extract< types >(attr); // Check the result if (val) { std::cout << "Extraction succeeded" << std::endl; switch (val.which()) { case 0: std::cout << "int: " << val.get< int >() << std::endl; break; case 1: std::cout << "string: " << val.get< std::string >() << std::endl; break; } } else std::cout << "Extraction failed" << std::endl; }
Обратите внимание, что мы использовали, который
метод обратной ссылки на отправку между возможными типами. Способ возвращает индекс типа в последовательноститипов
. Также обратите внимание, что методget
теперь принимает явный параметр шаблона для выбора эталонного типа для приобретения; естественно, этот тип должен соответствовать фактическому упомянутому типу, что оправдано утверждением переключателя / регистра в нашем случае.
Посещение ценности также поддерживается объектомvalue_ref
. Вот как мы вычисляем хеш-значение из извлеченного значения:
struct hash_visitor { typedef std::size_t result_type; result_type operator() (int val) const { std::size_t h = val; h = (h << 15) + h; h ^= (h >> 6) + (h << 7); return h; } result_type operator() (std::string const& val) const { std::size_t h = 0; for (std::string::const_iterator it = val.begin(), end = val.end(); it != end; ++it) h += *it; h = (h << 15) + h; h ^= (h >> 6) + (h << 7); return h; } }; void hash_value(logging::attribute_value const& attr) { // Define the set of expected types of the stored value typedef boost::mpl::vector< int, std::string > types; // Extract the stored value logging::value_ref< types > val = logging::extract< types >(attr); // Check the result if (val) std::cout << "Extraction succeeded, hash value: " << val.apply_visitor(hash_visitor()) << std::endl; else std::cout << "Extraction failed" << std::endl; }
Наконец, как и при посещении ценности, извлечение ценности также может применяться к записям журнала и наборам значений атрибутов.
void hash_value(logging::record_view const& rec, logging::attribute_name name) { // Define the set of expected types of the stored value typedef boost::mpl::vector< int, std::string > types; // Extract the stored value logging::value_ref< types > val = logging::extract< types >(name, rec); // Check the result if (val) std::cout << "Extraction succeeded, hash value: " << val.apply_visitor(hash_visitor()) << std::endl; else std::cout << "Extraction failed" << std::endl; }
Кроме того, библиотека предоставляет два специальных варианта функцииэкстракта
:экстракт_or_throw
иэкстракт_or_default
. Как следует из названия, функции обеспечивают различное поведение в случае, если значение атрибута не может быть извлечено. Первый бросает исключение, если значение не может быть извлечено, а второй возвращает значение по умолчанию.
![]() | Warning |
---|---|
Следует соблюдать осторожность при выполнении функции |
Аналогичнопосетите
, классатрибут_значение
имеет способы, названныеэкстракт
,экстракт_or_throw
иэкстракт_or_default
с тем же значением, что и соответствующие свободные функции, применяемые к значению атрибута.
#include <boost/log/attributes/scoped_attribute.hpp
>
Охваченные атрибуты являются мощным механизмом маркировки записей журнала, которые могут использоваться для различных целей. Как следует из названия, атрибуты сфер действия регистрируются в начале области действия и не регистрируются в конце области действия. Механизм включает в себя следующие макросы:
BOOST_LOG_SCOPED_LOGGER_ATTR
(logger, attr_name, attr);BOOST_LOG_SCOPED_THREAD_ATTR
(attr_name, attr);
Первый макрос регистрирует специфический атрибут источника врегистраторе
регистраторе объекта. Имя атрибута и сам атрибут даны в аргументахattr_name
иattr
. Второй макрос делает то же самое, но атрибут регистрируется для текущего потока в ядре для регистрации (что не требует регистратора).
![]() | Note |
---|---|
Если атрибут с тем же названием уже зарегистрирован в ядре logger/logging, макросы не будут переопределять существующий атрибут и в конечном итоге не будут иметь никакого эффекта. См.Обоснованиедля более подробного объяснения причин такого поведения. |
Пример использования следующий:
BOOST_LOG_DECLARE_GLOBAL_LOGGER(my_logger, src::logger_mt) void foo() { // This log record will also be marked with the "Tag" attribute, // whenever it is called from the A::bar function. // It will not be marked when called from other places. BOOST_LOG(get_my_logger()) << "A log message from foo"; } struct A { src::logger m_Logger; void bar() { // Set a thread-wide markup tag. // Note the additional parentheses to form a Boost.PP sequence. BOOST_LOG_SCOPED_THREAD_ATTR("Tag", attrs::constant< std::string >("Called from A::bar")); // This log record will be marked BOOST_LOG(m_Logger) << "A log message from A::bar"; foo(); } }; int main(int, char*[]) { src::logger lg; // Let's measure our application run time BOOST_LOG_SCOPED_LOGGER_ATTR(lg, "RunTime", attrs::timer()); // Mark application start. // The "RunTime" attribute should be nearly 0 at this point. BOOST_LOG(lg) << "Application started"; // Note that no other log records are affected by the "RunTime" attribute. foo(); A a; a.bar(); // Mark application ending. // The "RunTime" attribute will show the execution time elapsed. BOOST_LOG(lg) << "Application ended"; return 0; }
Довольно часто удобно отмечать группу регистрационных записей с постоянным значением, чтобы иметь возможность фильтровать записи позже. Библиотека предоставляет два удобных макроса только для этой цели:
BOOST_LOG_SCOPED_LOGGER_TAG
(logger, tag_name, tag_value);BOOST_LOG_SCOPED_THREAD_TAG
(tag_name, tag_value);
Макросы эффективно обертываются вокругBOOST_LOG_SCOPED_LOGGER_ATTR
иBOOST_LOG_SCOPED_THREAD_ATTR
соответственно. Например, атрибут «Tag» из приведенного выше примера может быть зарегистрирован следующим образом:
BOOST_LOG_SCOPED_THREAD_TAG("Tag", "Called from A::bar");
![]() | Warning |
---|---|
При использовании атрибутов с охватом убедитесь, что атрибут с охватом не изменен в наборе атрибутов, в котором он был зарегистрирован. Например, не следует очищать или переустанавливать набор атрибутов регистратора, если в нем зарегистрированы атрибуты, специфичные для регистратора. В противном случае программа может рухнуть. Эта проблема особенно важна в многопоточном приложении, когда один поток может не знать, есть ли атрибуты в регистраторе или нет. Будущие выпуски могут решить это ограничение, но в настоящее время атрибут должен оставаться нетронутым до тех пор, пока не будет зарегистрирован при выходе из сферы действия. |
Хотя описанные макросы предназначены для того, чтобы быть основным интерфейсом для функциональности, существует также интерфейс C++. Это может быть полезно, если пользователь решает разработать свои собственные макросы, которые не могут быть основаны на существующих.
Любое примечательное свойство прикрепляется к общему сторожевому объекту типаscoped_attribute
. Пока существует часовой, атрибут регистрируется. Существует несколько функций, которые создают часовые для атрибутов источника или потока:
// Source-specific scoped attribute registration template< typename LoggerT > [unspecified] add_scoped_logger_attribute( LoggerT& l, attribute_name const& name, attribute const& attr); // Thread-specific scoped attribute registration template< typename CharT > [unspecified] add_scoped_thread_attribute( attribute_name const& name, attribute const& attr);
Объект типаscoped_attribute
способен прикреплять результаты каждой из этих функций к своей конструкции. Например,BOOST_LOG_SCOPED_LOGGER_ATTRlg,"RunTime",attrs::таймер() может быть примерно расширен до этого:
attrs::scoped_attribute sentry = attrs::add_scoped_logger_attribute(lg, "RunTime", attrs::timer());
Статья Attributes раздела Chapter 1. Boost.Log v2 Detailed features description может быть полезна для разработчиков на c++ и boost.
Материалы статей собраны из открытых источников, владелец сайта не претендует на авторство. Там где авторство установить не удалось, материал подаётся без имени автора. В случае если Вы считаете, что Ваши права нарушены, пожалуйста, свяжитесь с владельцем сайта.
:: Главная :: Detailed features description ::
реклама |