Pull to refresh

Comments 90

UFO just landed and posted this here
Вот пишу я код вычислительной гидродинамики к примеру и как то не очень радуюсь таким выражениям:

U.setEnergy(V.getRho() * sqr(V.getV) / 2.0);

то ли дело

U.energy = V.rho * sqr(V.v) / 2.0; — просто и понятно.

А в реальном коде выражения гораздо развесистее, особенно когда в одной задаче и уравнения Максвелла и Навье-Стокса.

По началу тоже хотел использовать property примерно как в этом топике описано, но из-за того, что поддерживать неудобно, просто отказался от этой идеи и стал использовать поля c public доступом.
Сомнительное решение делать поля с public доступом…
Сахарок, не более того, в рантайме от этого всего толку мало в сравнении с Qtшными пропертями. И вообще, по моему мнению, проперти как раз нужны в рантайме, чтобы можно было свойства менять у заранее неизвестного типа.
Да да… Один только NOTIFY чего стоят… =)
UFO just landed and posted this here
UFO just landed and posted this here
9 из 10 программистов на C# вывехнули челюсть от смеха. Пока карма :-)
И программисты на Visual C++ тоже.
Забивать на переносимость ради сомнительной фичи?
В общем-то, и обычные программисты на C++.
Я программист на Visual C++, но глядя на костыль __declspec( property( get=get_func_name ) ) в конкретном компиляторе (прощай переносимость) мне совсем не смешно.
Я программист на Intel C++ и меня костыль __declspec(property) полностью устраивает.
Хорошо, на Visual C++ не смеются.
Да не, это любители переносимости, пищущие исключительно нетленку, только не смеются.
UFO just landed and posted this here
Не надо никого гнать в шею, а надо просто сесть и составить подробный документ, что из арсенала C++ у вас в проекте можно использовать и что нельзя. Это сработает лучше, чем постоянные одёргивания экспериментирующих коллег — никто не обидится и спорить не будет.
UFO just landed and posted this here
«Э-эх» — сказали смолтокеры, взвалили топоры на плечи и пошли рубить лес.

Это я к тому, что в Смолтоке пропертей нет и быть не может, но почему-то никто от этого не страдает.
И только старые лисперы усмехнулись в густые усы и промолчали, ибо язык, сковывающий каким-то предопределенным синтаксисом — неполноценен ;)
На самом деле все это было написано и опубликовано исключительно в учебных целях. В жизни я вижу одно применение этой штуки: с вас есть море написанного кода, где происходят обращения и полям класса, а вам надо все переделать, хоть в целях дебага, на сеттеры и геттеры. Это может быть не лучший выход из такой ситуации, но выход.
Эх, хотелось бы как-нибудь без init обходиться. Чтобы достаточно было в h-нике пару строк вписать и setter/getter работал. Подумаю на досуге над возможностью улучшения. Спасибо, интересно — плюсую)
А еще можно макрос написать, который «property int a» развернет в набор из сеттера и геттера. 4 строки займет и указывать класс не нужно.
UFO just landed and posted this here
Нука пример приведите?
Это шутка в лагере Си только действует, но не в плюсах.
На навскидку:

#include

#define PROPERTY(TYPE, NAME) \
public: TYPE get_##NAME() const { return property_##NAME##_; } \
public: void set_##NAME(const TYPE& NAME) { property_##NAME##_ = NAME; } \
private: TYPE property_##NAME##_

class A {
PROPERTY(int, a);
};

int main() {
A a;
a.set_a(5);
::std::cout
Ой, у меня тег code не работает (по техническим причинам), а в предпросмотре все было ок.

Потерялся: инклюд иострим и вывод get_a() в ::std::out.
UFO just landed and posted this here
Ну я не знаю, где Ваше Величество обучалось безхерновности, но тот же Александреску активно использует макросы. Просто библиотеки гугловые (gtest хотя бы) я вообще молчу.

Упорно не понимаю вашей нелюбви к макросам.
UFO just landed and posted this here
Ну все минусы являются следствием возможностей языка, иначе просто не сделать. Я в итоге написал свои свойства, но они похожи на данную реализацию, за некоторыми исключениями, так же сделал поддержку статических свойств, по сути отличие в том, что не требуется указатель на объект. Свой писал только в образовательных целях, сам давно привык к обычному С++ подходу с использованием set и get метода.

В Visual C++ есть свои свойства к примеру.
Почему-то никто не задумался о следующем негативном влиянии использования «пропертей» в С++, а именно: когда я вижу код вида a.b = c в С++ я по умолчанию считаю, что b это переменная, т.е. оверхед от обращения к ней минимален и я могу к ней обращаться таким образом сотни раз к примеру, а тут вдруг оказывается, что там каждый раз могут производится какие-то вычисления, которые могу быть отнюдь не быстрыми. В итоге это может сыграть с вами плохую шутку касательно производительности.
Производительность тут действительно страдает, причем даже без вычислений. Два вызова функции вместо одного на пустом месте в критической ситуации сыграют роль.
Совершенно верно, но я не стал уточнять такие подробности. Тем более, что описанный мной возможный вариант потенциально гораздно более опасен — представтье себе, что там делается обращение к базе данных, которая, не дай бог, еще и удаленная.
Это справедливо для всех языков, в которых есть свойства. Со свойствами всегда надо поаккуратнее. Я лично при многократном использовании значения свойства всегда кеширую его значение в стековую переменную, когда знаю, что его значение измениться не должно. Самая большая западлянка — обращение к свойству в цикле. На C# цикл обхода списка всегда пишу как:
IList list;
for (int i = 0, count = list.Count; i < count; i++)
{
}
Я написал про С++ только потому, что там этого нет на уровне встроенных языковых средств, в то время как в ряде других языков это возможно сделать «из коробки» и потому разработчик на таких языках как бы должен помнить об этом. Впрочем я согласен, что с этим надо быть аккуратно в любых языках. Я например считаю вредным использование декоратора @property в питоне по этой же причине.
выполняя операцию присваивания по умолчанию считаешь, что её никто не перегрузил? о_0"
А при чем тут операция присваивания? Речь про property. Не надо додумывать за меня то, о чем я не говорил.
В C++ как раз принято не доверять никому. Перегрузка операторов чего только стоит.
Если оператор сложения в коде делает вместо сложения умножение, это проблемы только кривого кода…
Здесь не стоит вопрос доверия. Вы можете не доверять сколько вам угодно. Речь про беглое чтение кода, которое делается как раз чаще всего и просто пожелание не использовать сомнительные приемы. С++ вообще как язык полон великого множества нюансов и подводных камней и хорошим стилем является как раз наимее проблемный код, а не особые выкрутасы, которые может и выглядят как элегантное решение в данный момент времени, но через месяц даже автор кода не вспомнит что там и как без нескольких часов колупания в коде.

когда я вижу код вида a.b = c в С++ я по умолчанию считаю, что b это переменная,

В нормально используемом C++ в 99% времени у вас нет доступа к полям напрямую (это же не Си), а только через методы.

т.е. оверхед от обращения к ней минимален и я могу к ней обращаться таким образом сотни раз к примеру, а тут вдруг оказывается, что там каждый раз могут производится какие-то вычисления, которые могу быть отнюдь не быстрыми.

Если бы в С++ были проперти наряду с обычными функциями, то никто бы не запрещал для "тяжелых" функций не использовать проперти, а оставлять из функциями. Лучше, когда выбор есть, чем когда его вообще нет

А что будет если от такого проперти указатель взять? Бяка похоже получится, т.к. приведение типа не сработает. Будет указатель на сам объект проперти.
Да, ничего хорошего не получится. Это даже не скомпилируется. Однако, странное желание взять указатель на свойство, ведь под свойством совсем не обязательно скрывается поле класса соответствующего типа. Поэтому даже логично, что такой код не компилируется.
С другой стороны, это мешает применять свойства как я описал комментарием выше;
Ну никто не мешает вам дописать в эту реализацию строки по перегрузке операции взятия адреса =)
Очень интересный способ потратить 90% времени не на сам проект, а на обустройство удобств дальнейшего программирования.

Вы удивитесь, но при создании любой библиотеки, 100% времени тратится на обустройство удобств дальнейшего программирования. Любая библиотека сама по себе даже не запускается

Мне не очень нравится, когда метод выглядит как переменная, если честно.
Тут как переменная выглядит как раз поле, а не метод.
Скрывается разбиение на 2 метода доступа к данным, делая обращение к ним более наглядным.
Сама концепция удобна, если поддерживается языком, а вот приведенные костыльные реализации для не поддерживающих это языков — сомнительны в плане своей оправданности.
Все равно неожиданно, когда, как кажется, меняешь поле, а на самом деле вызывается пачка методов. Это хорошо, что в примере простые сеттеры и геттеры. А если они на самом деле будут менять внутреннее состояние или еще что-нибудь цеплять?
Для плюсов такое поведение неестественно, это и смущает.
Как раз таки это скрытое поведение при изменении поля — очень большой плюс с точки зрения защищенности объекта и признак инкапсуляции (сокрытия необходимой, но не важной для пользователя классом, реализации поведения).

Наоборот крайне нежелательно давать прямой доступ к полям класса. Лучше, чтобы к полям был доступ через сокрытые setter/getter в виде проперти.

К тому же, с точки зрения терминологии, методы — это поведение класса, а на них вешают еще и запись/чтение полей класса. По мне так лучше, когда методы — это методы, а свойства(поля/проперти) — это свойства.
Зачем нужны эти самые проперти? В чем выгода перед обычными set/get() методами, я не понимаю.
Читабельность чуть выше. Плюс намного удобнее и интуитивно понятнее с незнакомыми классами, когда ищешь способ изменить какое-то свойство объекта, поскольку все свойства идут как бы отдельным набором от методов.
И на одно проперти приходится один путь доступа (одно имя) и на запись и на чтение.
Вот только у property на лбу, к сожалению, не написано ReadWrite она или ReadOnly. Это как раз уже минус.
А у кого написано? У Property можно без проблем посмотреть, а у обычных полей с методами set и get кроме как по отсутствию этих самых методов ничего и не скажешь.
Правильные «проперти» должны уметь:
1. Транзакции и валидацию. (если хотя бы одно свойство не удалось изменить по каким-либо причинам откатывается вся транзакция).
2. Транзакции должны уметь склеиваться. При этом выкидываются промежуточные значения (если таковые имеются). Это позволяет, например, удалять из UNDO изменения, экономя таким образом память, но оставляя возможность вернуться на много действий назад.
3. Свойства должны сами разбираться с типами. Преобразования типа string -> int должны выполняться автоматически.
4. Должна быть система ивентов, позволяющая навешивать обработчики на изменение свойств любых объектов. Например, «перерисовать окно, если отображаемое свойство изменилось».
5. Свойства и транзакции должны уметь сериализоваться. В этом случае другую сериализацию можно не писать в принципе (если все хранится в свойствах).
6. Любое изменение должно обязательно проходить валидацию. Причем, система валидации должна позволять ставить обработчики на валидацию свойств, которые могут выполнять дополнительные проверки и кидать исключения.
7. Свойства должны поддерживать контейнеры. Т.е. давать возможность писать что-то в духе std::for_each и т.п., но при этом все изменения должны попадать в транзакцию.
8. Использование свойств должно быть простым.
10. Также бывает крайне полезной возможность получать доступ к свойствам, через такие вещи как COM.

Вот как-то так, а иначе это детский лепет. Зачем нужны свойства без транзакций, я вообще лично не понимаю.

p.s. И да, это все реализуемо на С++.
Инфа про то, что правильные проперти должны всё это уметь. Или это просто ваше видение проблемы?
Не обязательно все, хотя бы что-то. Иначе от них нету никакого толку.
да, можно сказать, это мое видение проблемы
Почти по всем пунктам один вопрос: зачем? Всё что вы пишете, нужно очень небольшому числу программистов. Да и если уж вам так понадобилось всё это — реализуйте в сеттерах и геттерах, ну или расширяйте этот класс. А конкретно третий пункт, считаю не только бесполезным, но ещё и вредным. В C++ идеалогия строгой типизации, «само» приводится не должно. Компиляторы не зря варнинги кидают даже на неявное приведение float к int.
На вопрос «зачем?» ответ очень простой: «это удобно».

Property это не то, что используется внутри логики, там они не нужны и вредны (к тому же, они намного медленнее). Они используются для связи логики с внешним миром:

Примеры:

1. Undo/Redo
Когда у вас все на свойствах, вам вообще не нужно думать как оно работает. Когда пользователь из интерфейса чего-то меняет, меняются свойства. Каждый «apply» пишется в отдельную транзакцию (на уровне свойств). Если нужно сделать UNDO транзакция откатывается, если REDO накатывается.

Пример в коде:
// Указатель на объект, с которым работает интерфейс
// Детипизирован (интерфейс это не та штука, которая должна
// ломаться/перекомпилироваться при исправлении логики)
IPropertyEnabled *pSomeObj = ...;
 
// Значения, которые будем устанавливать, берутся, скажем из полей на форме.
int x = ...;
double y = ...;
std::string s = ...;
 
auto pCommand = 
 
    // Создаем новую пустую транзакцию
    MakeTransaction()
 
      // Открываем трамплин к объекту, все дальнейшие изменения будут происходить в нем
      // и мы не хотим писать указатель каждый раз
      .MakeTrampoline( pSomeObj )
 
      // Устанавливаем свойства, при каждом вызове происходит валидация конкретного свойства
      // (например, проверяется достоверность условия y >= 0, это делается на уровне логики
      // интерфейсу не положено знать, какие значения правильные в данном контексте)
      .SetPropertyValueAndValidate( "Property_X" /* имя свойства */, x )
      .SetPropertyValueAndValidate( "Property_Y" /* имя свойства */, y )
      .SetPropertyValueAndValidate( "Property_Z" /* имя свойства */, z )
 
      // закрываем трамплин к объекту
      .Close()
 
      // закрываем транзакцию, здесь происходит валидация всего объекта в целом
      // (например, может проверяться, что x < y)
      .Close();

В данном примере, имена свойств просто строки, часто их удобнее хранить в отдельных .h файлах как константы. Весь код, естественно, помещен в try блок, если происходит какая-то ошибка (неверное значение, неверный тип, неверное имя свойства и т.п.) выкидывается исключение содержащее подробную информацию о том что произошло. Если это ошибка на уровне кода (неверный тип) ее можно быстро исправить. При уничтожении незакрытой транзакции все изменения отменяются автоматически.
Полученная команда помещается в стек UNDO/REDO. Команда, предоставляет интерфейс для ее отката/наката.

2. Сериализация.
Если все ПОСТОЯННЫЕ данные хранятся в свойствах (это не означает, что логика не имеет к ним прямого доступа для чтение), то сериализация работает централизованно через свойства. В самих объектах ничего писать не надо. При этом лего добавляются новые виды хранилищ. Кеш и прочие временные данные в свойствах естественно не хранятся.

3. Четкое разделение интерфейса и логики.
Интерфейс работает только со свойствами. Интерфейс не имеет доступа к .h файлам логики в принципе, там и своей работы по рисованию «кнопочек» хватает. Это гарантирует работу таких вещей как UNDO из коробки. Вследствии детипизированности свойств, а также наличия трамплинов преобразования интерфейсу не нужно использовать точно такие же типы, как и логике, достаточно «похожих». Например, логика может использовать вектора с кастомными аллокаторами, а интерфейс с обычными.

4. Возможность построения сложных фабрик объектов.
Идея такая: создаем пустой объект, инициализируем его через свойства. Если что-то пошло не так мы об этом узнаем (есть валидация). Дальнейшие действия зависят от того, насколько объект нужен и т.п. Например, можно использовать в играх для добавления возможности конструирования кастомных танков игроками. На танк можно наложить общие ограничения (типа мощность выстрела не более ЭН), которые могут быть весьма сложными. Интерфейс в этом участвовать не будет.

5. За счет отсутствия жесткой связи между свойствами и их реализацией, очень легко пишутся врапперы, которые предоставляют доступ к свойствам из других языков. Например, из скриптового языка типа VBA/LUA.

6. Также легко пишутся Property Browser-ы. При удачном выборе имен свойств становится возможным делать вещи вроде «для всех объектов масштаб увеличить на 10%» (пример не очень удачен (это легко делается и через виртульные функции), но какой есть)

ну и т.п.
А что такое «трамплин»? Впервые встречаю этот термин в контексте программирования, стало очень любопытно.
Это маленькие функции, которые занимаются приведением аргументов к виду, понятному основной реализации. Например у вас есть некая функция, получающая вектор, а вы хотите, чтобы для одного значения её можно было вызвать без вектора. Если вы напишете функцию, которая это значение будет помещать в вектор и вызывать основную реализацию, то такая функция будет трамплином. В моем комментарии в коде создается трамплин, который ко всем вызовам добавляет указатель на объект.
Сразу видно, человек имел дело с реальными задачами.

А ещё очень интересны механизмы сквозного «склеивания» свойств в БД, слое бизнес-логики и представления. Когда одна и та же вещь и в БД, и в контроллере и в GUI называется одинаково. А то в enterprise-приложениях, чтобы добавить или изменить единственный «атрибут», приходится изменять массу кода и «пробрасывать» его через все слои абстракции. Имели опыт реализации подобной «серебрянной пули»?
>это все реализуемо на С++
Код в студию. Я уже предвкушаю продление жизни минимум на 10 лет.

Искренне не понимаю, зачем городить весь этот шаблонный хаос ради фич, для которых язык изначально не заточен? Есть же современные языки, гораздо более мощные, чем C++. Разговоры про суперпроизводительность — bullshit, т.к. большинство программистов не умеют писать эффективно ни на одном языке, а опытные и на Python напишут своё приложение, причём гораздо быстрее C++ников. И приложение будет работать с достаточной скоростью. Максимальная производительность большинству приложений не нужна. Нужно выжать последние мипсы-флопсы из проца — пишем критичный код на C.
Расширяемость языка тоже на очень низком уровне. Для элементарных задач метапрограммирования, которые у меня возникают очень часто, мощности C++ явно не хватает: примитивные текстовые макросы слишком слабы, а рекурсивные шаблоны смотрятся как спойлеры на «жигулях», или даже как пятая нога, торчащая из задницы.
Я из высокоуровневых языков использую D, Common Lisp и Python, а на C++ уже давно почти ничего не пишу, разве только для микроконтроллеров — там его использование ещё как-то оправдано. И вам советую перейти на более продвинутые языки, раз уж вам хочется, чтобы всё было, как у людей.
Код есть, работает все, кроме «трамплинов к контейнерам» (они немного не дописаны).

Насчет того, что удобнее просто взять другой язык согласен, но не всегда получается (кода много и там мат. моделирование, будет тормозить). К тому же property это как раз та штука, которая позволяет связать плюсы с другим языком.

Вообще-то в С++ принята традиция, что платить нужно только за то, что нужно. Так что утверждение должно-должно-должно, оно в корне неправильное. Если небольшому классу апликаций нужны транзакции или валидации, это прекрасно, но все остальные не в коем случае не должны получить оврхеад этих фич в нагрузку к требуемой более просто функциональности.

А описанные вами «правильно» и «должны», как мне кажется, относится к небольшому классу задач, к тому-же обычно реализуемому на языках, отличных от С++.
Естественно, такие вещи нужны в основном в больших приложениях. В остальных случаев обычных сеттеров и геттеров достаточно.

А что есть «большое приложение»? Игровой движок например — это большое приложение?
Зависит от размера движка =)
Имелось в виду приложение с большим количеством различных объектов, большим количеством настроек и сложной бизнес-логикой (в данном контексте — связями между «настройками»)

В игровых движках польза от свойств сомнительна (хотя если в игре можно «грабить корованы», то для контроля этого процесса их можно применить)

Проперти - это просто такой вид функции (только без скобочек). Все остальное - Ваши выдумки

Мне одному кажется, что property в подобной реализации, мало того что уже тысячу раз писались, так еще и не нужны?
Нет не одному, вот человек тоже считает что это не надо. А по поводу того, что тысячу раз писалось, гуглится на эту тему не так много. А так, не плохое упражнение, а если дописать под себя, то и в реальных проектах можно использовать.
Насчет тяжело гуглится, я думаю вы какой-то другой гугл используете. Я когда писал МАНовскую работу в классе 11м без проблем нашел реализацию подобную Вашей.

В любом случае, упражняться, это конечно хорошо, но упражняться в том, что по большому счету, нигде не нужно — я думаю, это как минимум расточительно.
Модифицируем ваш пример немного, точнее его использование
class TestClass
{
public:
...
void _setStr(std::string s)
{
propStr = s;
}
Property<std::string, TestClass, WriteOnly> testStr;

private:
...
std::string propStr;
};

int main()
{
TestClass t;
{
TestClass *t1 = new TestClass;
t1->testStr = "1";
t = *t1;
delete t1;
}
t.testStr = "12";
...
return 0;
}


И все падант. Почему? В пропертях нет конструктора копирования, а есть поинтер на владельца.
Не зря на собеседованиях по C++ много гоняют на конструкторы \ деструкторы. В определенный момент подобные лаги выхватываешь взглядом на первом проходе глазками по коду :)

Так же хотелось бы как-то избегать падений если я забыл вызвать Init для проперти, ну человеческое исключение хотяб кидать, а не просто вылетать по битому указателю.

Сеттеры хотелось бы писать в стиле
void _setStr(const std::string &s)
{
propStr = s;
}

т.е. передавать значения по ссылке, не все пропертя могут быть легкими интегральными типами, мб. и тяжелые объекты
>В стратье вы найдёте один из возм
исправь:)
Объектно-ориентированные и структурные системы склонны подходить к проблемам с диаметрально противоположных направлений. Возьмите в качестве примера скромную запись employee. В структурных системах вы бы использовали тип struct и имели бы доступ к полям этого типа повсюду
из своей программы. Например, код для печати записи мог бы свободно повторяться в нескольких
сотнях мест программы. Если вы меняете что-то в основе, вроде изменения типа поля name с массива
char на 16-битные символы Unicode, то вы должны разыскать каждую ссылку на name и
модифицировать ее для работы с новым типом.

В хорошо спроектированной объектно-ориентированной системе было бы невозможно получить
доступ к полю name.
Позвольте мне повторить это, потому что эта концепция так фундаментальна:
невозможно получить доступ к полю внутри объекта, даже такому простому, как name в объекте
employee. Скорее всего вы попросите employee проявить какую-нибудь способность, такую как
«напечатать себя», «сохранить себя в базе данных» или «модифицировать себя, взаимодействуя с
пользователем». В этом последнем случае обработчик сообщений вывел бы диалоговое окно, которое
бы использовалось пользователем для ввода или изменения данных.


Ален Голуб, «Правила программирования С++».

Рекомендую очень внимательно отнестись к данному подходу.
Его «подсказывает» сам синтаксис: либо мы заводим простейшие структуры с открытыми для доступа членами (для семантической группировки данных в структуру), либо используем классы с закрытыми по умолчанию членами — как самостоятельно действующие единицы программы. Несмотря на одинаковую реализацию, это совершенно разные сущности.

Класс хранит всё внутри и работает как чёрный ящик, выполняя высокоуровневые запросы, обращённые к нему (открытые функции-члены). В соответствии с правилами инкапсуляции (которые, поверьте, имеют смысл), всё остальное закрыто для внешнего мира.

В этом смысле, стоит задуматься, что представляет собой реализуемое свойство (property) класса. Это действительно необходимость, то есть некое сообщение, запрос действия от класса? Или это просто усложнённый и завуалированный вариант открытого члена класса?
слишком много дополнительных переменных для проперти.
все смещения можно посчитать и так:
#include <iostream>
using namespace std;
#define this_base(type,member) ((type *)(this - offsetof(type,member)))

class C{
    struct prop_x{
        operator int()const {   
            return this_base(C,x)->x_getter();  
        }
        prop_x & operator=(int xx){ 
            this_base(C,x)->x_setter(xx); return *this; 
        }
    };
    int _x;
    int x_getter()const{ 
        cout<<"getter "<<_x<<endl; return 0; 
    }
    void x_setter(int xx){ 
        cout<<"setter "<<xx<<endl; _x = xx;
    }
public:
    prop_x x;
};

int main(){
    C c;
    c.x = 5;
    int q = c.x;
}

но лучше в структуру свойства положить какие-то данные, а то 4 байта зря пропадет.
Но все же интересно, почему offsetof требует standard-layout type, и ругается (варнингом) на non-standard-layout type «C»?
багфикс:
    int x_getter()const{ 
        cout<<"getter "<<_x<<endl; return _x; 
    }
Ну если сахар, то для большего удобства данный вариант инициализации можно заменить
Property<int, TestClass, ReadWrite> testRW;
...
testRW.init(this, &TestClass::_getterRW, &TestClass::_setterRW);


как-то вот так, ближе к стилю BCB

typedef Property<int, TestClass, ReadWrite> PropInt;
...
PropInt testRW = { this, &_getterRW, &setterRW };
...


(CodeBlock / MinGW-gcc)
Sign up to leave a comment.

Articles