понедельник, 30 марта 2015 г.

[prog.c++.flame] Об "убийцах" C++ на примере Zimbu

На днях довелось вспомнить, что есть, оказывается, такой язык -- Zimbu. И что писал о нем уже чуть больше пяти лет назад.

За прошедшее время Zimbu дошел до стадии, которую его автор характеризует как нечто среднее между "proof-of-concept" и версией 1.0. Т.е., при существующей динамике, еще годик-полтора-два и можно ждать релиза версии 1.0.

Но примечательно не это, а пример, сравнивающий код на Zimbu с кодом на C++ (на всякий случай в виде скриншота):

Пример такой, что имеет смысл поговорить о нем подробнее.

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

#include <iostream>
#include <vector>
#include <memory>
 
using namespace std;

class Animal
{
public:
    virtual void eat() const {
        cout << "I eat like a generic Animal." << endl;
    }
    virtual ~Animal() {}
};
 
class Wolf : public Animal
{
public:
    void eat() const override {
        cout << "I eat like a wolf!" << endl;
    }
};
 
class Fish : public Animal
{
public:
    void eat() const override {
        cout << "I eat like a fish!" << endl;
    }
};
 
class GoldFish : public Fish
{
public:
    void eat() const override {
        cout << "I eat like a goldfish!" << endl;
    }
};
 
 
class OtherAnimal : public Animal
{
};
 
int main()
{
    vector< unique_ptr< Animal > > animals;
    animals.push_back( make_unique< Animal >() );
    animals.push_back( make_unique< Wolf >() );
    animals.push_back( make_unique< Fish >() );
    animals.push_back( make_unique< GoldFish >() );
    animals.push_back( make_unique< OtherAnimal >() );
 
    forauto & a : animals ) 
       a->eat();
 
   return 0;
}

Сразу же видны мелкие и не очень улучшения:

  • нет явных вызовов new/delete. Т.е. память будет подчищаться автоматически, не нужно бояться забыть написать delete;
  • нет объемного for-а для итерации по коллекции, который пришлось бы писать в C++03;
  • за счет ключевого слова override компилятор проверяет корректность переопределения виртуальных методов в наследниках.

Все это не очень большие изменения в языке. Но решающие реальные проблемы и серьезно упрощающие жизнь разработчику.

Но этот код лично мне не кажется красивым. Его простота, как говорят, хуже воровства. Добавление элементов в контейнер слишком многословно. Можно ли это устранить? Легко:

templatetypename A, typename C, typename... P >
void add_animal( C & to, P &&... params )
{
   to.push_back( make_unique< A >( forward< P >(params)... ) );
}
 
int main()
{
    vector< unique_ptr< Animal > > animals;
    add_animal< Animal >( animals );
    add_animal< Wolf >( animals );
    add_animal< Fish >( animals );
    add_animal< GoldFish >( animals );
    add_animal< OtherAnimal >( animals );
 
    forauto & a : animals ) 
       a->eat();
 
   return 0;
}

Одна простая вспомогательная функция и прикладной код существенно сокращается в объеме, а за счет этого, оказывается понятнее и проще. Причем это достигается за счет возможностей, которые были добавлены в язык как раз тогда, когда Брам Мууленар начал работать над Zimbu в стремлении создать более простой и удобный инструмент, чем C++. А именно: variadic templates и rvalue references (а так же ставший возможным благодаря им perfect forwarding).

И тут возникает вполне резонный вопрос: а если в программе на Zimbu захочется сделать похожий рефакторинг, получится ли это? Можно ли будет написать такую шаблонную функцию add_animal, которая сможет создавать разных наследников класса Animal. И добавлять их в разные типы коллекций? Например, в C++ном коде можно сменить vector на list или deque, но ни саму функцию add_animal, ни ее вызовы, менять не придется. А как в Zimbu? Вроде как шаблонов там до сих пор нет.

О чем все это говорит? Возможно о том, что создающиеся то тут, то там, убийцы C++ сильно опоздали. Они не успели возникнуть в тот момент, когда C++ находился в стагнации (где-то между 2004-м и 2010-м годами, по моим субъективным впечатлениям). Появись тогда мощный и удобный инструмент на замену C++, возможно все сложилось бы иначе. Но не случилось, и теперь каждому "убийце C++" нужно конкурировать уже не с текущим вариантом C++, а с тем, который будет поддержан основными C++ными компиляторами через 2-3-года.

Т.е. потенциальный "убийца C++" уже сейчас должен быть сильно лучше, чем будущий C++. Причем прямо здесь, и прямо сейчас: чтобы и компилятор был шустрый, стабильный и с кодогенерацией не хуже C++ных компиляторов; чтобы библиотек было полно и работать с ними было удобно; чтобы средства разработки были на разный вкус и цвет; чтобы поддерживался этот язык на большом спектре платформ... Если этого нет (а ведь этого нет), то новому языку потребуется несколько лет, чтобы обзавестись всем этим. Скажем, когда появился язык Go? Пять лет назад? Вот столько же потребуется какому-нибудь Rust-у или Zimbu.

Сильно сомневаюсь, что все это время C++ опять будет топаться на месте. Скорее он будет двигаться вперед. Конечно же, принципиально другим языком C++ не станет. Тяжелые родовые травмы никуда не денутся. Но, с другой стороны, в этом тоже есть положительный момент -- это значит, что старый код, переписывать который никто просто так не будет, продолжит работать. А сопровождать и обновлять его будет все проще и проще. Это уже сейчас так: C++11 намного удобнее в повседневной работе, чем C++03. C++14 удобнее C++11. Следующий стандарт, C++17, должен стать еще удобнее. Что говорит о том, что C++ный код можно было спокойно писать вчера, можно спокойно писать сегодня и можно будет спокойно писать завтра. Чего не скажешь о его потенциальных "убийцах", кого бы на эту роль не назначали: D, Go, Zimbu или Rust.

Комментариев нет: