Виртуальный конструктор: Виртуальный конструктор / Хабр

Содержание

Виртуальные функции и деструктор / Хабр

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

Сразу же, как обычно, оговорюсь, что: 1) статья моя не претендует на полноту изложения материала; 2) мегапрограммеры ничего нового здесь не узнают; 3) материал не новый и давно описан во многих книгах, но если явно об этом не прочитать и самому специально не задумываться, то можно о некоторых моментах даже не подозревать (до поры, до времени). Также прошу прощения за надуманные примеры 🙂

Виртуальные деструкторы

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

Основное правило: если у вас в классе присутствует хотя бы одна виртуальная функция, деструктор также следует сделать виртуальным. При этом не следует забывать, что деструктор по умолчанию виртуальным не будует, поэтому следует объявить его явно. Если этого не сделать, у вас в программе почти наверняка будут утечки памяти (memory leaks). Чтобы понять почему, опять же много ума не надо. Рассмотрим несколько примеров.

В первом случае создадим объект производного класса в стеке:

#include <cstdlib>
#include <iostream>

using std::cout;
using std::endl;

class A {
public:
    A() { cout << "A()" << endl; }
    ~A() { cout << "~A()" << endl; }
};

class B : public A {

public:
    B() { cout << "B()" << endl; }
    ~B() { cout << "~B()" << endl; }
};

int main()
{
    B b;
    return EXIT_SUCCESS;
}



Всем ясно, что вывод программы будет следующим:

A()
B()
~B()
~A()

потому что сначала конструируется базовая часть класса, затем производная, а при разрушении наоборот — сначала вызывается деструктор производного класса, который по окончании своей работы вызывает по цепочке деструктор базового. Это правильно и так должно быть.

Попробуем теперь создать тот же объект в динамической памяти, используя при этом указатель на объект базового класса (код классов не изменился, поэтому привожу только код функции main()):

int main()
{
    A * pA = new B;
    delete pA;
    return EXIT_SUCCESS;
}

На сей раз конструируется объект так, как и надо, а при разрушении происходит утечка памяти, потому как деструктор производного класса не вызывается:

A()
B()
~A()

Происходит это потому, что удаление производится через указатель на базовый класс и для вызова деструктора компилятор использует раннее связывание. Деструктор базового класса не может вызвать деструктор производного, потому что он о нем ничего не знает. В итоге часть памяти, выделенная под производный класс, безвозвратно теряется.

Чтобы этого избежать, деструктор в базовом классе должен быть объявлен как виртуальный:

#include <cstdlib>
#include <iostream>

using std::cout;
using std::endl;

class A {
public:
    A() { cout << "A()" << endl; }
    virtual ~A() { cout << "~A()" << endl; }
};

class B : public A {
public:
    B() { cout << "B()" << endl; }

    ~B() { cout << "~B()" << endl; }
};

int main()
{
    A * pA = new B;
    delete pA;
    return EXIT_SUCCESS;
}


Теперь-то мы получим желаемый порядок вызовов:

A()
B()
~B()
~A()

Происходит так потому, что отныне для вызова деструктора используется позднее связывание, то есть при разрушении объекта берется указатель на класс, затем из таблицы виртуальных функций определяется адрес нужного нам деструктора, а это деструктор производного класса, который после своей работы, как и полагается, вызывает деструктор базового. Итог: объект разрушен, память освобождена.

Виртуальные функции в деструкторах

Давайте для начала рассмотрим ситуацию с вызовом виртуальных функций внутри класса. Предположим, что у нас есть Кот, который просит покушать мяуканьем, а затем приступает к процессу 🙂 Так поступают многие коты, но не Чеширский! Чеширский, как известно, мало того что вечно улыбается, так еще и довольно разговорчив, поэтому мы научим его говорить, переопределив метод speak():

#include <cstdlib>
#include <iostream>

using std::cout;
using std::endl;

class Cat
{
public:
    void askForFood() const
    {
        speak();
        eat();
    }
    virtual void speak() const { cout << "Meow! "; }
    virtual void eat() const { cout << "*champing*" << endl; }
};

class CheshireCat : public Cat
{
public:
    virtual void speak() const { cout << "WTF?! Where\'s my milk? =) "; }
};

int main()
{
    Cat * cats[] = { new Cat, new CheshireCat };

    cout << "Ordinary Cat: "; cats[0]->askForFood();
    cout << "Cheshire Cat: "; cats[1]->askForFood();

    delete cats[0]; delete cats[1];

    return EXIT_SUCCESS;
}


Вывод этой программы будет следующим:

Ordinary Cat: Meow! *champing*
Cheshire Cat: WTF?! Where’s my milk? =) *champing*

Рассмотрим код более подробно. Есть класс Cat с парой виртуальных методов, один из которых переопределен в производном CheshireCat. Но всё самое интересное происходит в методе askForFood() класса Cat.

Как видно, метод всего лишь содержит вызовы двух других методов, однако конструкция speak() в данном контексте эквивалента this->speak(), то есть вызов происходит через указатель, а значит — будет использовано позднее связывание. Вот почему при вызове метода askForFood() через указатель на CheshireCat мы видим то, что и хотели: механизм виртуальных функций работает исправно даже несмотря на то, что вызов непосредственно виртуального метода происходит внутри другого метода класса.

А теперь самое интересное: что будет, если попытаться воспользоваться этим в деструкторе? Модернизируем код так, чтобы при деструкции наши питомцы прощались, кто как умеет:

#include <cstdlib>
#include <iostream>

using std::cout;
using std::endl;

class Cat
{
public:
    virtual ~Cat() { sayGoodbye(); }
    void askForFood() const
    {
        speak();
        eat();
    }
    virtual void speak() const { cout << "Meow! "; }
    virtual void eat() const { cout << "*champing*" << endl; }
    virtual void sayGoodbye() const { cout << "Meow-meow!" << endl; }
};

class CheshireCat : public Cat
{
public:
    virtual void speak() const { cout << "WTF?! Where\'s my milk? =) "; }
    virtual void sayGoodbye() const { cout << "Bye-bye! (:" << endl; }
};

int main()
{
    Cat * cats[] = { new Cat, new CheshireCat };

    cout << "Ordinary Cat: "; cats[0]->askForFood();
    cout << "Cheshire Cat: "; cats[1]->askForFood();

    delete cats[0]; delete cats[1];
    return EXIT_SUCCESS;
}


Можно ожидать, что, как и в случае с вызовом метода speak(), будет выполнено позднее связывание, однако это не так:

Ordinary Cat: Meow! *champing*
Cheshire Cat: WTF?! Where’s my milk? =) *champing*
Meow-meow!
Meow-meow!

Почему? Да потому что при вызове виртуальных методов из деструктора компилятор использует не позднее, а раннее связывание. Если подумать, зачем он делает именно так, всё становится очевидным: нужно просто рассмотреть порядок конструирования и разрушения объектов. Все помнят, что конструирование объекта происходит, начиная с базового класса, а разрушение идет в строго обратном порядке. Таким образом, когда мы создаем объект типа CheshireCat, порядок вызовов конструкторов/деструкторов будет таким:

Cat()
CheshireCat()
~CheshireCat()
~Cat()

Если же мы захотим внутри деструктора ~Cat() совершить виртуальный вызов метода sayGoodbye(), то фактически попытаемся обратиться к той части объекта, которая уже была разрушена.

Мораль: если в вашей голове витают помыслы выделить какой-то алгоритм «зачистки» в отдельный метод, переопределяемый в производных классах, а затем виртуально вызывать его в деструкторе, у вас ничего не выйдет.

C++ MythBusters. Миф о виртуальных функциях (дополнение) / Хабр

Преамбула

Добрый вечер (ну, или кому чего).

Не так давно наткнулся на хабре на статью о виртуальных функциях в С++ (она находится сейчас тут). Был бы рад добавить комментарий, но, как оказалось, тут надо иметь регистрацию. Собственно поэтому я и написал этот пост-дополнение к вышеуказанной статье.

В данной статье я хочу затронуть вопрос виртуальности конструкторов, деструкторов, а также специфичные вопросы, так или иначе связанные с виртуальностью функций.

Статья расчитана на программистов средней и высокой квалификации. Приятного чтения.

Виртуальные конструкторы в C++

Итак, пожалуй начнем с конструкторов. Тут все очень просто — виртуальных конструкторов (а также похожих на них конструкторов) в C++ не существует. Просто потому что не бывает и всё тут (конкретно: это запрещено стандартом языка).

Вы, наверно, спросите: «а зачем такое вообще может понадобится?». На самом деле разговор про «виртуальный конструктор» немного некорректны. Конструктор не может быть виртуальным в смысле виртуальных функций, т.к. чтобы была виртуальность необходимо в конструкторе (а больше и негде особо) настроить указатель на ТВМ (таблицу виртуальных функций) для создаваемого объекта.

Замечание: обычно виртуальность реализуется через ТВМ и указатель на нее в объекте. Подробнее вы можете прочесть об этом тут

Так вот, иногда «виртуальным конструктором» называют механизм создания объект любого заранее неизвестного класса. Это может пригодится, например, при копировании массива объектов, унаследованных от общего предка (при этом нам бы очень хотелось чтобы вызывался конструктор копирования именно нужного нам класса, конечно же). В C++ для подобного, обычно, используют виртуальную функцию вроде

virtual void assign (const object &o), или подобную, однако, это не является повсеместным, и возможны другие реализации.

Виртуальный деструктор

А вот деструктор, напротив может быть виртуальным. И даже более того — это часто встречается.
Обычным является использование вирт деструктора в классах, имеющих вирт функции. Более того, gcc, например, выдаст вам предупреждение, если вы не сделаете виртуальным деструктор, объявив виртуальную функцию.

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

Другой миф: чисто виртуальных деструкторов не бывает. Ещё как бывают.

class Sample {
public:
virtual ~Sample()=0;
};

* This source code was highlighted with Source Code Highlighter.


Существует миф, что данный класс является абстрактным. И это верно.
Также распространено заблуждение, что налсденики этого класса будут полиморфными. Это неверно — деструкторы не наследуются.
Существут миф, что наследкника этого класса создать нельзя. Можно, вот пример:
class Sample {
public:
virtual ~Sample()=0{} //обратите особое внимание сюда, так писать по стандарту нельзя, но MS VC проглотит
};

class DSample: public Sample {

};

* This source code was highlighted with Source Code Highlighter.

Для вменяемых компиляторов класс Sample нужно писать так:

class Sample {
public:
virtual ~Sample()=0;
};

Sample::~Sample() {
}


* This source code was highlighted with Source Code Highlighter.

Сразу же замечание про нормальность компилятора, и заодно миф: по стандарту определять чисто вирт. функцию внутри определения класса нельзя. Но определенные корпорации говорят «если мы видим возможность улучшить стандарт, то мы незадумываясь делаем это».

Вы ниразу не видели чисто виртуальных деструкторов с определенными «телами»? Так вот, миф что они несуществуют, также неверен. Также определять можно и другие чисто виртуальные функции.

Почему надо писать именно с определением деструктора? Ответ на самом деле прост: из налсденика DSample в его деструкторе ~DSample будет вызываться деструктор ~Sample, и поэтому его необходимо определить, иначе у вас это даже не будет компилироваться.

Для чего нужен подобный чисто виртуальный деструктор? Это используется для того, чтобы сделать класс абстрактным, не создавая чисто виртуальных функций. Другого применения я найти не смог.

Замечания об устройстве указаталей на функцию-член

Казалось бы данная часть не имеет отношения к виртуальности. Если вы так думаете, то сильно заблуждаетесь. Вообще говоря указатели на функцию член в C++ используются не так часто. Отчасти это связано с мутными (как мне кажется) главами в стандарте языка, отчасти, потому что их реализация выливается в полный кошмар для программистов компиляторов. Мне не известно ни одного компилятора, который мог бы работать с этими «штуками» полностью по стандарту.

Если вы дейтвительно хотите знать как это устроено внутри (заодно сломать мозги), то советую обратиться по адресу www.rsdn.ru/article/cpp/fastdelegate.xml, там все очень подробно описано. Только не говорите что я вас не предупреждал.

Заключение

Вот, наверно, и все что я хотел бы рассказать вам в дополнение. Конечно, часть вопросов, связанных с виртуальностью все еще требуют открытий, оставляю это на вашей совести 8)

Удачи в программировании.

P.S. перенести бы в CPP блог надо бы… думаю там оно будет более востребовано.

Опенсорс и эксперименты с виртуальным конструктором LEGO

Моё детство примерно на 20% состояло из Dungeons & Dragons (D&D) и на 80% — из LEGO. Эти два занятия очень сильно пересекались. Мне, по разным причинам, не разрешали всё время играть в D&D. Но я, привлекая на помощь воображение, и достигнув в этом деле успехов, достойных плута 15 уровня, понял, что создание персонажей AD&D игрой не считается. Воссоздание вселенной DragonLance средствами LEGO очень хорошо помогало мне быть ближе к игре, которая мне очень нравилась.

Поэтому одним из моих любимых направлений в LEGO были замки. Я тратил многие часы, выдумывая подземелья для моих героев. Для того чтобы не терять свои находки, и из-за того, что я видел, как мои друзья в школе чертят карты подземелий, я составлял планы своих LEGO-моделей на миллиметровке. Кроме того, я пытался сохранить и сведения о том, как именно были устроены модели. Использование миллиметровки казалось логичным для изображения того, что, в основном, состояло из прямоугольных блоков. Но меня, в итоге, сгубило недостаточно хорошее понимание правил изометрической проекции.

Теперь, хоть я и стал старше, моя любовь к LEGO не угасла. И хотя я и не могу сказать, что очень уж горжусь своими моделями (их называют MOC-моделями), я почувствовал, что просто должен разобраться с тем, как мне документировать то, что создаю. Я никогда не умел очень уж хорошо рисовать. Поэтому я решил обратиться к компьютеру.

CAD для LEGO


Несколько лет я работал в сфере виртуального 3D-моделирования (а в сфере обычного 3D — и того больше). Я хорошо владею 3D-приложениями, но всё, чем я пользовался, заточено под анимированную графику и под производство фильмов. Все эти программы, как, собственно, и фильмы, рассчитаны на то, чтобы создать красивую картинку. Как именно что-то сделано, до тех пор, пока всё выглядит хорошо, не так уж и важно. Если, ради того, чтобы что-то выглядело бы очень хорошо, нужно «обмануть» законы физики, то это вполне приемлемо, так как это будет существовать только в виртуальном пространстве.

А вот системы автоматизированного проектирования (Computer-Aided Design, CAD), это уже нечто другое. CAD-приложения пришли на смену обычным чертежам. В них создают спецификации, иллюстрирующие то, как нечто может быть создано в реальном мире. От этих программ ждут точности и реализма.

Так как невероятно много людей увлечено LEGO, существует активное сообщество тех, кто создаёт LEGO-модели, используя CAD-программы. Преимущества такого подхода очевидны: можно задокументировать подробные сведения о модели, описать то, какие детали нужны для её создания, и то, как именно их нужно соединить друг с другом. Это, конечно, не замена реальному конструктору LEGO (ну, разве что для тех, кто любит CAD больше, чем LEGO), но это — отличное дополнение к хобби.

Для того чтобы построить виртуальную модель LEGO, нужны две вещи:

  • Виртуальные детали LEGO.
  • CAD-приложение.

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

Виртуальные детали LEGO


Для того чтобы раздобыть виртуальное представление практически любого из когда-либо созданных строительных блоков для LEGO-моделей, можете воспользоваться опенсорсным ресурсом LDraw. LDraw — это открытый стандарт для цифровых моделей LEGO, который включает в себя возможности по описанию размеров и ориентации элементов. В дополнение к работе по описанию деталей средствами LDraw, силами сообщества подготовлены 3D-модели для каждой детали. Это значит, что все желающие могут загрузить тысячи определений деталей, истратив на это не особенно много трафика.

Установка набора деталей


Виртуальные детали очень похожи на изображения, которые используются на сайтах, или на шрифты, применяемые на компьютере. Собственно говоря, соответствующие файлы можно хранить где угодно. Главное, чтобы приложение, в котором планируется работать с деталями, знало о том, где эти файлы находятся. В Linux LDraw-файлы обычно размещают в папке /usr/share/LDRAW. В Windows это обычно C:\Users\Public\Documents\LDraw.

LDraw даёт в наше распоряжение лишь спецификации для каждой детали. Вот, например, как выглядит код описания кубика 1×1:

0 ~Brick  1 x  1 without Front Face
0 Name: s\3005s01.dat
0 Author: John Riley [jriley]
0 !LDRAW_ORG Subpart UPDATE 2004-01
0 !LICENSE Redistributable under CCAL version 2.0 : see CAreadme.txt

0 BFC CERTIFY CCW

0 BFC INVERTNEXT
1 16 0 24 0 0 0 6 0 -20 0 -6 0 0 box5.dat
4 16 10 24 -10 6 24 -6 6 24 6 10 24 10
4 16 10 24 10 6 24 6 -6 24 6 -10 24 10
4 16 -10 24 10 -6 24 6 -6 24 -6 -10 24 -10
4 16 -10 24 -10 -6 24 -6 6 24 -6 10 24 -10
1 16 0 24 0 10 0 0 0 -24 0 0 0 10 box4t.dat
1 16 0 0 0 0 0 1 0 1 0 -1 0 0 stud.dat
0

Для того чтобы увидеть детали в более привычном облике, понадобится программа для их визуализации.

Приложение LDView для визуализации деталей


LDView — это среда для 3D-рендеринга, напоминающая POV-Ray или Cycles из Blender. Это приложение создано специально для рендеринга .ldr-файлов, то есть — CAD-файлов, содержащих данные в формате LDraw.

Если вы работаете на Linux, то, возможно, вы найдёте LDView в своём репозитории ПО. Если в репозитории этой программы не окажется — вы можете скачать установщик с сайта проекта. Если вы пользуетесь macOS или Windows, то вам, опять же, нужно будет воспользоваться сайтом LDView.

Просмотр отдельной детали


Легче всего начать цифровое конструирование моделей LEGO, попытавшись визуализировать отдельную деталь.

Сначала откройте ваш любимый текстовый редактор. Это может быть любая программа. Главное — чтобы она могла сохранять документы в виде обычного текста. Некоторые текстовые редакторы, в стремлении оказать пользователям добрую услугу, пытаются сохранять текстовые материалы в файлах, в которых, помимо текстов, есть ещё масса служебной информации (вроде .rtf и .doc). Существует множество хороших кросс-платформенных текстовых редакторов. Я, для наших дел, могу порекомендовать довольно-таки минималистичный редактор Geany.

Создадим новый файл с именем 1brick.ldr и введём в него следующий текст:

0 Name: 1brick.ldr
0 Author: Seth Kenlon

0 clr  x y z  a b c  d e f   g h i <file>
1   1  0 0 0  0 0 1  0 1 0  -1 0 0 3001.dat

А теперь взглянем на наше скромное творение:
$ LDView 1brick.ldr


Кубик LEGO

Только что вы создали простой CAD-файл, описывающий один кубик (а именно — модель номер 3001), цветовой индекс которого равняется 1 (это синий цвет), расположенный в позиции (0, 0, 0) по осям X, Y и Z. Поворот кубика регулируется с использованием средств матричного преобразования. Их применение, надо признать, не относится к простым математическим вычислениям. Правда, при конструировании LEGO-моделей произвольное вращение деталей требуется сравнительно редко, так как большинство деталей стыкуются друг с другом с использованием шипов.

Любая строка в файле, начинающаяся с 0, содержит либо комментарий, либо метаданные. Строка, начинающаяся с 1, содержит описание детали.

Вы можете попрактиковаться в перемещении и вращении деталей, внося изменения в свой CAD-файл. Обычный кубик имеет в высоту 24 LDU (LDraw Units). Это значит, что ставить детали друг на друга можно, меняя их координату Y с шагом в 24 единицы. Поворачивать детали можно, выполняя матричные преобразования.

Взгляните на этот код:

0 Name: 1brick.ldr
0 Author: Seth Kenlon

0 clr  x y z  a b c  d e f   g h i  file
1   1  0 0 0  0 0 1  0 1 0  -1 0 0  3001.dat
1   2 0 24 0 -1 0 0  0 1 0   0 0 -1 3001.dat

Вот результат его визуализации.
Два кубика

Конечно, перемещать детали можно вдоль любой из трёх осей. В спецификации LDraw сказано, что кубик 1×1 имеет 20 LDU в ширину и 20 LDU в длину. А это значит, что расставлять такие кубики вдоль оси X можно, меняя их позиции с шагом в 20 LDU.

0 Name: 1brick.ldr
0 Author: Seth Kenlon

0 clr  x y z  a b c  d e f   g h i  file
1   1  0 0 0  0 0 1  0 1 0  -1 0 0  3001.dat
1   2 0 24 0 -1 0 0  0 1 0   0 0 -1 3001.dat


Ещё два кубика

Порядок сборки модели


Чаще всего формат LDraw используется для того чтобы продемонстрировать порядок сборки модели. А это значит, что нужно описать последовательность шагов сборки. В LDraw это делается с использованием метакоманды STEP.

Для того чтобы испытать эту метакоманду, добавьте в свой файл, между описаниями деталей, следующее:

0 STEP

Готовый файл будет выглядеть так:
0 Name: 1brick.ldr
0 Author: Seth Kenlon

0 clr  x y z  a b c  d e f   g h i  file
1   1  0 0 0  0 0 1  0 1 0  -1 0 0  3001.dat

0 STEP

1   2 0 24 0 -1 0 0  0 1 0   0 0 -1 3001.dat

Теперь в вашем проекте описано два шага. На первом выводится первый кубик, на втором — второй. Можно пошагово просматривать .ldr-файлы, пользуясь клавишами-стрелками в верхней панели инструментов LDView, находящимися около подписи Steps.
Панель инструментов для пошаговой визуализации моделей

На одном шаге необязательно должен выводиться лишь один кубик. Как и в случае с инструкциями к наборам LEGO, установку нескольких деталей можно объединить в один шаг. Главное, чтобы это не повредило понятности инструкции.

В LDraw есть и другие команды. Например — тут можно рисовать линии, поясняющие расположение деталей, и делать прочие подобные вещи. Соответствующие сведения можно найти в спецификации.

Выяснение кодов деталей


Я хранил свою коллекцию LEGO в ящиках для рыболовных принадлежностей. Поэтому я мог быстро найти любую деталь из любого набора. Правда, по мере того, как росла коллекция, мне было нужно всё больше и больше ящиков. А в результате у меня стало уходить больше времени на поиск нужной детали.

Если учесть то, что в LEGO имеется более 11000 уникальных деталей, искать цифровые детали так же сложно, как и обычные. У каждой официальной детали LEGO есть собственный код. Например, тот кубик 2×4, который мы использовали в примере, имеет код 3001. Если вам известен код детали, вы можете просто использовать его в CAD-файле, и соответствующая деталь появится в вашей модели.

В дистрибутиве LDraw имеется файл parts.lst, в котором, с помощью grep, можно найти нужную деталь. Но детали там не всегда описаны по одной и той же схеме. Работая с этим файлом не всегда легко предугадать то, какие именно ключевые слова соответствуют тем или иным деталям. Например — как понять, какое слово, «curved» «sloped» или «angled», лучше всего характеризует некую деталь сложной формы?

Хотя искать детали можно и в parts.lst, в этом деле нам могут помочь некоторые специальные интернет-ресурсы:

  • Lugnet — это пользовательская группа, в которой есть база данных со сведениями о кодах деталей LEGO, построенная на основе сведений, взятых из LDraw.
  • BrickLink — хороший каталог деталей.
  • Rebrickable — ещё один ресурс, на котором есть каталог деталей.

Другие средства для рендеринга моделей


После того, как вы создали свой шедевр, LDView может экспортировать вашу модель, что позволит вам отрендерить её в высоком качестве. Для этого можно воспользоваться POV-Ray — опенсорсной программой для фотореалистичного рендеринга трёхмерных моделей. В результате плоды ваших трудов можно будет представить в весьма привлекательном виде. Найти POV-Ray можно или в репозитории программ вашего дистрибутива Linux, или на сайте проекта.

Вот пример команды рендеринга:

$ povray +I1brick.pov +Q11 +W4196 +h3160 +O1brick-high.png

Ниже показан результат визуализации.
Высококачественная визуализация модели

Если вам нужна программа для формирования инструкций по сборке моделей — попробуйте опенсорсную LPub3D. Эта программа выводит пошаговые инструкции и список деталей, необходимых на каждом шаге.


LPub3D

Исследование мира LEGO


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

Кроме того, эксперименты с цифровым вариантом LEGO позволяют создавать виртуальные модели, которые могут быть очень сложными, и могут включать в себя любые детали, даже такие, которых нет у создателя модели. Цифровые детали LEGO можно использовать для создания анимаций, для подготовки иллюстраций сложных моделей, или даже для проектирования собственных деталей. В Сети есть несколько сообществ любителей LEGO, и многие из них, вроде BrickHub.org, публикую прекрасные рендеры, в основе которых лежат LDraw-файлы.

Мир любителей LEGO — это приятное и креативное место, которое стоит посетить всем тем, кому нравится создавать цифровые модели, разрабатывать собственные детали, или делать с кубиками LEGO что-то такое, чего никто больше с ними не делает. Если вам нравится LEGO, то сегодня — самый лучший день для того чтобы стать частью LEGO-сообщества!

А вам нравится LEGO?

Руководство для начинающих VR-разработчиков / Блог компании Mail.ru Group / Хабр

В этом руководстве собраны базовые ссылки и рекомендации, которые могут послужить вам точкой отсчёта в освоении VR-разработки.


Спросите себя: меня интересует разработка для десктопных устройств, наподобие HTC Vive, или меня больше привлекают мобильные устройства вроде Samsung Gear VR или Google Cardboard? Если вы пока не определились, то почитайте обзоры и подумайте о том, что лучше выбрать для вашего рынка. Если для ваших идей требуются контроллеры движения или качественная графика, то ориентируйтесь на подключаемые к компьютеру очки VR. Модели, которые сегодня поддерживаются движками Unity, Unreal и веб-реaлизациями:

Компьютерная VR:
Мобильная VR: (в качестве базового устройства может использоваться смартфон)
Веб-реализация виртуальной реальности: (в качестве базового устройства может использоваться смартфон)
  • Язык разработки Mozilla A-Frame (как HTML и XML) для создания кроссплатформенных VR-приложений. Чтобы понять, как это выглядит, зайдите на сайт со своего смартфона, отключите блокировку ориентации и нажмите появившуюся кнопку VR.
  • Vizor — веб-приложение, позволяющее создавать 3D-сцены и просматривать их на разных платформах, включая мобильные устройства. Конечно, возможностей у него меньше, чем у игровых движков или открытых веб-платформ, но зато оно очень простое и позволяет легко начать изучать создание виртуальной реальности без дорогих устройств. В блоге есть несколько вводных постов.
  • Responsive WebVR — кроссплатформенный веб-инструмент, доступный для модифицирования. Возможно, вы захотите освежить его с помощью Three.js.

Пока не выпущенное:
  • Google Daydream. Недоступно, но уже поддерживается в Unreal Engine 4, доступна предварительная техническая версия в Unity.
  • OSVR HDK 2, $399. Выйдет в июле, не упомянут контроллер движения.


Дизайн для VR очень похож на дизайн видеоигр, поскольку в обоих случаях мы имеем дело с интерактивным 3D-опытом. Разница в том, что в VR нужно уделять особое внимание эффекту присутствия, погружённости, нелинейности повествования, не вызывающему тошноты перемещению и графической оптимизации.

Большинство VR-разработчиков предпочитают использовать игровые движки (если только не создают для веб-VR, о чём ниже), и с самого начала им приходится выбирать, на чём же работать. Самые популярные движки — Unreal Engine 4 (UE4) и Unity. Оба имеют очень широкие возможности и являются надёжными инструментами. Вокруг обоих сложились активные сообщества с многочисленными информационными ресурсами. Оба движка позволяют управлять 3D-окружением, импортировать собственный контент (3D-модели, изображения, звук, видео), а также программировать интерактивность и геймплей. На YouTube есть огромное количество обучающих видео, а в сети — руководств, созданных как самими авторами, так и поклонниками.

Среди VR-разработчиков нет общепринятого мнения, что один из этих движков лучше другого. У каждого есть свои особенности. UE4 считается более оптимизированным с точки зрения вычислений, даёт более достоверную картинку, но имеет более крутую кривую обучения. Unity создавался из расчёта, чтобы его возможностей хватало для создания коммерческих игр, но при этом он остаётся более интуитивно понятным и эффективным для начинающих разработчиков. Unreal Engine 4 можно скачать и использовать бесплатно, но авторам придётся ежеквартально отстёгивать по 5% дохода с игры, если он превысит $3000. У Unity есть несколько версий разной стоимости, но можно остановиться на бесплатной Unity Personal. Желательно попробовать оба движка, чтобы понять, какой вам подходит больше, хотя здесь трудно ошибиться, потому что вы в любом случае получаете превосходный и мощный инструмент.

Помимо игровых движков, вы можете обратиться к разработке интерактивных VR-веб-страниц. Это можно делать с помощью языка разметки Mozilla’s A-Frame, с помощью JavaScript (поковыряйтесь в Three.js!), HTML5 и/или WebGL. Подобные эксперименты ведутся в Chrome и Mozilla. Разработка для веба позволяет отображать VR-контент прямо на смартфонах пользователей, так что вам не понадобится дорогое дополнительное оборудование. Также вам не придётся компилировать или упаковывать код, вы легко можете делиться своими творениями с друзьями. Если вам всё это кажется слишком трудоёмким, то можете начать с простейшего редактора VR-сцен Vizor, позволяющего рисовать на компьютере и просматривать с мобильных устройств.

После того, как вы определитесь с движком или веб-приложением, надо поподробнее ознакомиться со своим выбором. Начните с азов того языка программирования, который использует ваш инструмент: C++ и Blueprints Visual Scripting (UE4), C# (Unity) или кастомный язык разметки для веб-приложений. Если вы разрабатываете для Android, то скачайте Android Studio и попробуйте развернуть тренировочное приложение. В случае с Google Cardboard и Unity обратитесь к Google SDK.

В /learnVRdev wiki есть ссылки и материалы, полезные для тех, кто учится использовать движки. Лучше знакомиться с движком по какому-нибудь руководству, чтобы лучше прочувствовать его, как манипулировать объектами в пространстве, и так далее. В Unity и Unreal есть встроенный предпросмотр, так что вы можете сразу увидеть, что у вас получилось!


Итак, вы выбрали движок и обзавелись VR-устройством. Теперь вам нужен графический контент, аудио материалы, 3D-модели и анимации для заполнения виртуального мира. Всё это можно найти в сети, надёргать из популярных игр (если вы не планируете продавать свой продукт), сделать самостоятельно или модифицировать готовые материалы. Помните, что виртуальная реальность требует максимально реалистичного визуального и звукового оформления при близком исследовании, с разных сторон, даже если объект стилизован или абстрактен.

3D-модели


У начинающих есть два пути.
  1. Самый простой: использовать открыто доступные 3D-модели, пока вы изучаете другие аспекты VR-разработки. Можно использовать содержимое хранилищ ресурсов (asset stores) Unity и Unreal, либо поискать на сторонних сайтах. У начинающего и так голова забита множеством новой информации, так что лучше таким образом упростить себе процесс обучения.
  2. Другой вариант: научиться делать 3D-модели самостоятельно. Это труднее, но в долгосрочной перспективе лучше. Ведь со временем ваши проекты будут усложняться, и рано или поздно вам понадобятся собственные арт-материалы.

Даже если вы решили взять уже готовые исходники, возможно, в результате вы захотите подправить их в 3D-редакторе. К счастью, для этого есть достаточно онлайн-ресурсов. Профессиональными инструментами можно пользоваться по ежемесячной подписке, сравнимой с абонентской платой за MMORPG. И в сети есть руководства по всем вопросам 3D-моделирования (в первую очередь, на YouTube). Используйте поиск на каждом сайте! Если вам нужен более качественные обучающие материалы, то можете подписаться на PluralSight. Немало полезного можно найти и на Reddit, в обсуждениях различных VR-сообществ.
  • 3D-моделирование:
    • Autodesk’s Entertainment Creation Suite. Пакет приложений (включающий в себя Maya, 3ds Max, Motionbuilder и Mudbox, с нативным экспортом в Unity и UE4) доступен для «студентов» бесплатно в течение трёх лет. При этом никакой проверки на «студенчество» не делается. В этом пакете есть всё, что нужно для создания профессиональных моделей, текстур, анимаций и так далее.
    • Pixologic ZBrush (от $795, студентам — скидка). Это приложение для создания 3D-скульптур, дающее больше творческой свободы, чем традиционные приложения вроде Maya или 3ds Max. Оно позволяет создавать и обрабатывать высокополигональные, фотореалистичные модели. Функциональность аналогична Autodesk Mudbox.
    • Blender. Бесплатный пакет opensource-приложений для 3D-моделирования, анимации и игрового дизайна. У него очень широкие возможности, но он гораздо сложнее в освоении, чем коммерческое ПО.
    • Покупать и скачивать модели и 3D-сканы можно на сайтах Turbosquid и Sketchfab.
    • MODO Indie ($15 в месяц, или $300). Инструмент для 3D-моделирования, раскраски и анимации, предназначенный для игровых дизайнеров и любительского моделирования.
    • Speedtree ($19 в месяц). Приложение полезно для создания процедурно генерируемых моделей деревьев, растений и прочих ветвистых структур. Их можно извлечь со всевозможными опциями для использования в фотореалистичных ландшафтах.

Фотограмметрия (3D-сканирование)


Как и VR, трёхмерное фотосканирование — это ещё одна футуристическая технология, уже доступная для использования в дешёвых мобильных решениях. Фотограмметрия — это использование многочисленных фотографий настоящих объектов с разных ракурсов для построения их моделей. Фотографии импортируются в приложения вроде Agisoft Photoscan, или одно из многочисленных решений от Autodesk, и на их основе генерируются подробные сетчатые модели. Затем их вместе с цветовыми/диффузными текстурными картами можно экспортировать и использовать в игровом движке в качестве регулярного ресурса. Весь процесс хорошо показан на YouTube.
  • Фотограмметрия и 3D-сканирование
    • Agisoft Photoscan (от $179). Набор приложения для 3D-сканирования, где в качестве источника данных используются фотографии.
    • Autodesk предлагается несколько разных решений, от бесплатных мобильны и облачных (123D Catch) до десктопных (Remake и Recap 360). Здесь обсуждаются различия между разными программами.

Аудио и музыка


Работа со звуковыми эффектами в VR не слишком отличается от работы над музыкой и эффектами в кино и традиционных играх. Как и в случае с графикой, нужно делать упор на реализм и качество. Наибольшая степень погружения достигается с помощью размещения источников звука относительно позиции игрока, направления его взгляда. Чтобы Unity и UE4 корректно функционировали с точки зрения звука, их придётся настраивать.
  • Создание аудио


После того, как вы освоитесь с движком и приготовите арт-материалы, нужно будет придумать, как придать вашему проекту интерактивности. Я очень рекомендую сначала почитать о принципах построения UI и UX в виртуальной реальности. Иначе у ваших пользователей могут заболеть глаза от плохих решений по стереоскопическому рендерингу, или их укачает. Этого можно избежать, просто отказавшись от привязки текста к полю просмотра, или поместив камеру игрока во время движения в видимую капсулу (автомобиль, скафандр, кабину). А если вы хотите реализовать ручное управление, то рекомендую делать всё как можно реалистичнее — ваши усилия по исследованию и прототипированию будут вознаграждены чувством присутствия.

Полезные ресурсы по UI/UX в виртуальной реальности


Вам потребуется освоить некое подобие скриптового языка. В Unreal Engine 4 используется интуитивно понятная, схематическая скриптовая система Blueprint Visual Scripting. К слову, она будет полезна для тех, кто ещё не слишком уверенно чувствует себя в программировании вообще. Общее введение в Blueprint, эта система достаточно мощная, чтобы с её помощью сделать весь проект, не написав ни строчки кода (хотя вы и будете использовать ряд программистских методик). А вообще в Unreal используется С++, а в Unity — C#. Многие из тех, кто стремится войти в VR-разработку, имеют очень мало опыта программирования, так что этот этап становится особенно трудным.
Если вы самостоятельный разработчик, помните — лучше начинать с малого. Когда вы освоите базовые вещи, можно будет переходить к более масштабным идеям. Но начните лучше с самого примитивного проекта. Развивайтесь поэтапно, создав несколько проектов, вы сможете гораздо увереннее штурмовать более сложные задачи.

Опасности конструкторов / Хабр

Привет, Хабр! Представляю вашему вниманию перевод статьи «Perils of Constructors» автора Aleksey Kladov.

Один из моих любимых постов из блогов о Rust — Things Rust Shipped Without авторства Graydon Hoare. Для меня отсутствие в языке любой фичи, способной выстрелить в ногу, обычно важнее выразительности. В этом слегка философском эссе я хочу поговорить о моей особенно любимой фиче, отсутствующей в Rust — о конструкторах.


Конструкторы обычно используются в ОО языках. Задача конструктора — полностью инициализировать объект, прежде чем остальной мир увидит его. На первый взгляд, это кажется действительно хорошей идеей:


  1. Вы устанавливаете инварианты в конструкторе.
  2. Каждый метод заботится о сохранении инвариантов.
  3. Вместе эти два свойства значат, что можно думать об объектах как об инвариантах, а не как о конкретных внутренних состояниях.

Конструктор здесь играет роль индукционной базы, будучи единственным способом создать новый объект.

К сожалению, в этих рассуждениях есть дыра: сам конструктор наблюдает объект в незаконченном состоянии, что и создает множество проблем.


Когда конструктор инициализирует объект, он начинает с некоторого пустого состояния. Но как вы определите это пустое состояние для произвольного объекта?

Наиболее легкий способ сделать это — присвоить всем полям значения по умолчанию: false для bool, 0 для чисел, null для всех ссылок. Но такой подход требует, чтобы все типы имели значения по умолчанию, и вводит в язык печально известный null. Именно по этому пути пошла Java: в начале создания объекта все поля имеют значения 0 или null.

При таком подходе будет очень сложно избавиться от null впоследствии. Хороший пример для изучения — Kotlin. Kotlin использует non-nullable типы по умолчанию, но он вынужден работать с прежде существующей семантикой JVM. Дизайн языка хорошо скрывает этот факт и хорошо применим на практике, но несостоятелен. Иными словами, используя конструкторы, есть возможность обойти проверки на null в Kotlin.

Главная характерная черта Kotlin — поощрение создания так называемых «первичных конструкторов», которые одновременно объявляют поле и присваивают ему значение прежде, чем будет выполняться какой-либо пользовательский код:

class Person(
  val firstName: String,
  val lastName: String
) { ... }

Другой вариант: если поле не объявлено в конструкторе, программист должен немедленно инициализировать его:

class Person(val firstName: String, val lastName: String) {
    val fullName: String = "$firstName $lastName"
}

Попытка использовать поле перед инициализацией запрещена статически:

class Person(val firstName: String, val lastName: String) {
    val fullName: String
    init {
        println(fullName) // ошибка: переменная должна быть инициализирована
        fullName = "$firstName $lastName"
    }
}

Но, имея немного креативности, любой может обойти эти проверки. Например, для этого подойдет вызов метода:

class A {
    val x: Any
    init {
        observeNull()
        x = 92
    }
    fun observeNull() = println(x) // выводит null
}

fun main() {
    A()
}

Также подойдет захват this лямбдой (которая создается в Kotlin следующим образом: { args -> body }):

class B {
    val x: Any = { y }()
    val y: Any = x
}

fun main() {
    println(B().x) // выводит null
}

Примеры вроде этих кажутся нереальными в действительности (и так и есть), но я находил подобные ошибки в реальном коде (правило вероятности 0-1 Колмогорова в разработке ПО: в достаточно большой базе любой кусок кода почти гарантированно существует, по крайней мере, если не запрещен статически компилятором; в таком случае он почти точно не существует).

Причина, по которой Kotlin может существовать с этой несостоятельностью, та же, что и в случае с ковариантными массивами в Java: в рантайме все равно происходят проверки. В конце концов, я бы не хотел усложнять систему типов Kotlin, чтобы сделать вышеприведенные случаи некорректными на этапе компиляции: учитывая существующие ограничения (семантику JVM), отношение цена/польза проверок в рантайме намного лучше таковой у статических проверок.

А что, если язык не имеет разумного значения по умолчанию для каждого типа? Например, в C++, где определенные пользователем типы не обязательно являются ссылками, вы не можете просто присвоить null каждому полю и сказать, что это будет работать! Вместо этого в C++ используется специальный синтаксис для установления начальных значений полям: списки инициализации:

#include <string>
#include <utility>

class person {
  person(std::string first_name, std::string last_name)
    : first_name(std::move(first_name))
    , last_name(std::move(last_name))
  {}

  std::string first_name;
  std::string last_name;
};

Так как это специальный синтаксис, остальная часть языка работает с ним небезупречно. Например, сложно поместить в списки инициализации произвольные операции, так как C++ не является фразированным языком (expression-oriented language) (что само по себе нормально). Чтобы работать с исключениями, возникающими в списках инициализации, необходимо использовать еще одну невразумительную фичу языка.


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

Особенно странные вещи происходят, когда конструктор базового класса вызывает метод, переопределенный в производном классе:

abstract class Base {
    init {
        initialize()
    }
    abstract fun initialize()
}

class Derived: Base() {
    val x: Any = 92
    override fun initialize() = println(x) // выводит null!
}

Просто подумайте об этом: код произвольного класса выполняется до вызова его конструктора! Подобный код на C++ приведет к еще более любопытным результатам. Вместо вызова функции производного класса будет вызвана функция базового класса. Это имеет немного смысла, потому что производный класс еще не был инициализирован (помните, мы не можем просто сказать, что все поля имеют значение null). Однако если функция в базовом классе будет чистой виртуальной, ее вызов приведет к UB.


Нарушение инвариантов — не единственная проблема конструкторов. Они имеют сигнатуру с фиксированным именем (пустым) и типом возвращаемого значения (сам класс). Это делает перегрузки конструкторов сложными для понимания людьми.


Вопрос на засыпку: чему соответствует std::vector<int> xs(92, 2)?

a. Вектору двоек длины 92

b. [92, 92]

c. [92, 2]

Проблемы с возвращаемым значением возникают, как правило, тогда, когда оказывается невозможно создать объект. Вы не можете просто вернуть Result<MyClass, io::Error> или null из конструктора!

Это часто используется в качестве аргумента в пользу того, что использовать C++ без исключений сложно, и что использование конструкторов вынуждает также использовать исключения. Однако, я не думаю, что этот аргумент корректен: фабричные методы решают обе эти проблемы, потому что они могут иметь произвольные имена и возвращать произвольные типы. Я считаю, что следующий паттерн иногда может быть полезен в ОО языках:


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


  • для публичного API предоставляются публичные фабричные методы с подходящими названиями и типами возвращаемых значений.


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

trait Default {
    fn default() -> Self;
}

trait Clone {
    fn clone(&self) -> Self;
}

В Rust есть только один способ создать структуру: предоставить значения для всех полей. Фабричные функции, такие как общепринятый new, играют роль конструкторов, но, что самое важное, они не позволяют вызывать какие-либо методы до тех пор, пока у вас на руках нет хотя бы более-менее корректного экземпляра структуры.

Недостаток этого подхода заключается в том, что любой код может создать структуру, так что нет единого места, такого как конструктор, для поддержания инвариантов. На практике это легко решается приватностью: если поля структуры приватные, то эта структура может быть создана только в том же модуле. Внутри одного модуля совсем нетрудно придерживаться соглашения «все способы создания структуры должны использовать метод new». Вы даже можете представить расширение языка, которое позволит помечать некоторые функции атрибутом #[constructor], чтобы синтаксис литерала структуры был доступен только в помеченных функциях. Но, опять же, дополнительные языковые механизмы мне кажутся излишними: следование локальным соглашениям требует мало усилий.


Лично я считаю, что этот компромисс выглядит точно также и для контрактного программирования в целом. Контракты вроде «не null» или «положительное значение» лучше всего кодируются в типах. Для сложных инвариантов просто писать assert!(self.validate()) в каждом методе не так уж и сложно. Между этими двумя паттернами есть немного места для #[pre] и #[post] условий, реализованных на уровне языка или основанных на макросах.

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

Во-первых, в Swift используются именованные аргументы, и это немного помогает с «все конструкторы имеют одинаковое имя». В частности, два конструктора с одинаковыми типами параметров — не проблема:

Celsius(fromFahrenheit: 212.0)
Celsius(fromKelvin: 273.15)

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

В-третьих, на уровне языка есть поддержка конструкторов, вызов которых может завершиться неудачей. Конструктор может быть обозначен как nullable, что делает результат вызова класса вариантом. Конструктор также может иметь модификатор throws, который лучше работает с семантикой двухфазной инициализации в Swift, чем с синтаксисом списков инициализации в C++.

Swift удается закрыть в конструкторах все дыры, на которые я пожаловался. Это, однако, имеет свою цену: глава, посвященная инициализации одна из самых больших в книге по Swift.


Вопреки всему я могу придумать как минимум две причины, по которым конструкторы не могут быть замещены литералами структуры, такими как в Rust.

Во-первых, наследование в той или иной степени вынуждает язык иметь конструкторы. Вы можете представить расширение синтаксиса структур с поддержкой базовых классов:

struct Base { ... }

struct Derived: Base { foo: i32 }

impl Derived {
    fn new() -> Derived {
        Derived {
            Base::new()..,
            foo: 92,
        }
    }
}

Но это не будет работать в типичном макете объектов (object layout) ОО языка с простым наследованием! Обычно объект начинается с заголовка, за которым следуют поля классов, от базового до самого производного. Таким образом, префикс объекта производного класса является корректным объектом базового класса. Однако, чтобы такой макет работал, конструктору необходимо выделять память под весь объект за один раз. Он не может просто выделить память только под базовый класс, а затем присоединить производные поля. Но такое выделение памяти по кускам необходимо, если мы хотим использовать синтаксис создания структуры, где мы могли бы указывать значение для базового класса.

Во-вторых, в отличие от синтаксиса литерала структуры, конструкторы имеют ABI, хорошо работающий с размещением подобъектов объекта в памяти (placement-friendly ABI). Конструктор работает с указателем на this, который указывает на область памяти, которую должен занимать новый объект. Что самое важное, конструктор может с легкостью передавать указатель в конструкторы подобъектов, позволяя тем самым создавать сложные деревья значений «на месте». В противовес этому, в Rust конструирование структур семантически включает довольно много копий, и здесь мы надеемся на милость оптимизатора. Это не совпадение, что в Rust еще нет принятого рабочего предложения относительно размещения подобъектов в памяти!

Upd 1: исправил опечатку. Заменил «литерал записи» на «литерал структуры».

Программируем роботов — бесплатный робосимулятор V-REP. Первые шаги

Программирование роботов — это интересно.

Многие наверное видели японских гуманоидных роботов, или французский учебный робот NAO, интересным выглядит проект обучаемого робота-манипулятор Baxter. Промышленные манипуляторы KUKA из Германии — это классика. Кто-то программирует системы конвейерной обработки (фильтрации, сортировки). Дельта роботы. Есть целый пласт — управление квадрокоптером/алгоритмы стабилизации. И конечно же простые трудяги на складе — Line Follower.

Но всё это как правило — не дешевые игрушки, поэтому доступ к роботам есть в специализированных лабораториях или институтах/школах где получили финансирование и есть эти направления. Всем же остальным разработчикам (кому интересна робототехника) — остаётся завистливо смотреть.

Некоторое время назад я вышел на достаточно интересную систему — 3д робосимулятор V-REP, от швейцарской компании Coppelia Robotics.

К своему (приятному) удивлению я обнаружил, что эта система:

  • имеет большой функционал (система разрабатывается с марта 2010 года)
  • полностью open-source (выложена в открытый доступ в 2013 году)
  • кроссплатформенная — windows, mac, linux (работает на Qt)
  • имеет API и библиотеки для работы с роботами через C/C++, Python, Java, Lua, Matlab, Octave или Urbi
  • бесплатная для некоммерческого использования!

Все объекты, которые программируются в этой системе — «живут» в реальном с точки зрения физических законов мире — есть гравитация, можно захватывать предметы, столкновения, датчики расстояния, видео датчики и т.п.

Поработав некоторое время с этой системой, я решил рассказать про неё читателям хабра.

Да, и на картинке скриншот из V-REP, и модели роботов — которые вы можете программировать, и смотреть поведение, прямо на вашем компьютере.

Установка

Установим на компьютер эту систему, в разделе Download:

Видим три варианта: образовательный (EDU), триальный (EVAL), и плеер (player).

Плеер — это программа с помощью которой можно проиграть сцены созданные в полноценной версии (то есть нет возможности редактирования) — бесплатная.

Триальная — это полнофункциональная версия, в которой нет возможности сохранить. Нет лицензионных ограничений.

Образовательный — это полнофункциональный пакет, имеющий лицензионные ограничения, текст лицензии можно прочитать здесь. Суть его в том, что институты, школы, хоббисты — могут использовать бесплатно это программное обеспечение. При том, что использование не коммерческое (а образовательное).

Мы с вами вполне подходим под определение хоббистов (т.к. хотим образовательно по-программировать роботов), поэтому смело скачиваем версию EDU PRO для своей операционной системы.

В данный момент версия 3.2.0, вот прямая ссылка на windows вариант: V-REP_PRO_EDU_V3_2_0_Setup (98 Mb)

Старт

После установки, и старта мы увидим экран:

Здесь мы видим следующие объекты:

— сцена — здесь и происходит всё действо, на данный момент она пуста (есть только пол)
— слева видим блок с библиотекой моделей — сверху папки, и под ней — отображается содержимое выбранной папки (выбраны robots/non-mobile — то есть стационарные роботы — манипуляторы)
— далее отображается иерархия мира

Иерархия включает в себя — корневой объект (мир), в котором находятся все объекты.

В нашем примере это:

Видим источники света, видим объект для реализации пола (а это твердая поверхность, с текстурой), и группу для камер.

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

Вверху и слева мы видим toolbar — меню. Самой главной кнопкой является кнопка Play (Start Simulation) — после которой стартует симуляция сцены:

Сценарий работы следующий:
— мы перетаскиваем с помощью DragAndDrop объекты из библиотеки моделей.
— корректируем их местоположение
— настраиваем скрипты
— стартуем симулятор
— останавливаем симулятор

Попробуем что-нибудь на практике.

Быстрый старт

Попробуем оживить робота.

Для этого выбираем слева папку robots/mobile и в списке выбираем Ansi, захватываем, переносим на сцену и отпускаем, робот появляется на нашей сцене и появляется информация об авторе:

Теперь нажимаем на Start Simulation, и видим движение робота, и можем управлять положение головы, рук (реализовано через Custom User Interface), вот видео:

Далее останавливаем симуляцию:

Скрипт управления

Можем открыть и увидеть код, который научил робота идти (управляет автономным передвижением робота). Для этого на иерархии объектов, напротив модели Asti, дважды кликаем на иконке «файл»:

Вот Lua программа, которая осуществляет движение робота:

Скрипт управления движением робота Asti
if (sim_call_type==sim_childscriptcall_initialization) then 
	asti=simGetObjectHandle("Asti")
	lFoot=simGetObjectHandle("leftFootTarget")
	rFoot=simGetObjectHandle("rightFootTarget")
	lPath=simGetObjectHandle("leftFootPath")
	rPath=simGetObjectHandle("rightFootPath")
	lPathLength=simGetPathLength(lPath)
	rPathLength=simGetPathLength(rPath)
	ui=simGetUIHandle("astiUserInterface")
	simSetUIButtonLabel(ui,0,simGetObjectName(asti).." user interface")
	dist=0
	correction=0.0305
	
	minVal={0,			-- Step size
			0,			-- Walking speed
			-math.pi/2,	-- Neck 1
			-math.pi/8,	-- Neck 2
			-math.pi/2,	-- Left shoulder 1
			0,			-- Left shoulder 2
			-math.pi/2,	-- Left forearm
			-math.pi/2,	-- Right shoulder 1
			0,			-- Right shoulder 2
			-math.pi/2}	-- Right forearm
	rangeVal={	2,			-- Step size
				0.8,		-- Walking speed
				math.pi,	-- Neck 1
				math.pi/4,	-- Neck 2
				math.pi/2,	-- Left shoulder 1
				math.pi/2,	-- Left shoulder 2
				math.pi/2,	-- Left forearm
				math.pi/2,	-- Right shoulder 1
				math.pi/2,	-- Right shoulder 2
				math.pi/2}	-- Right forearm
	uiSliderIDs={3,4,5,6,7,8,9,10,11,12}

	relativeStepSize=1
	nominalVelocity=0.4
	neckJoints={simGetObjectHandle("neckJoint0"),simGetObjectHandle("neckJoint1")}
	leftArmJoints={simGetObjectHandle("leftArmJoint0"),simGetObjectHandle("leftArmJoint1"),simGetObjectHandle("leftArmJoint2")}
	rightArmJoints={simGetObjectHandle("rightArmJoint0"),simGetObjectHandle("rightArmJoint1"),simGetObjectHandle("rightArmJoint2")}
		
	-- Now apply current values to the user interface:
	simSetUISlider(ui,uiSliderIDs[1],(relativeStepSize-minVal[1])*1000/rangeVal[1])
	simSetUISlider(ui,uiSliderIDs[2],(nominalVelocity-minVal[2])*1000/rangeVal[2])
	simSetUISlider(ui,uiSliderIDs[3],(simGetJointPosition(neckJoints[1])-minVal[3])*1000/rangeVal[3])
	simSetUISlider(ui,uiSliderIDs[4],(simGetJointPosition(neckJoints[2])-minVal[4])*1000/rangeVal[4])
	simSetUISlider(ui,uiSliderIDs[5],(simGetJointPosition(leftArmJoints[1])-minVal[5])*1000/rangeVal[5])
	simSetUISlider(ui,uiSliderIDs[6],(simGetJointPosition(leftArmJoints[2])-minVal[6])*1000/rangeVal[6])
	simSetUISlider(ui,uiSliderIDs[7],(simGetJointPosition(leftArmJoints[3])-minVal[7])*1000/rangeVal[7])
	simSetUISlider(ui,uiSliderIDs[8],(simGetJointPosition(rightArmJoints[1])-minVal[8])*1000/rangeVal[8])
	simSetUISlider(ui,uiSliderIDs[9],(simGetJointPosition(rightArmJoints[2])-minVal[9])*1000/rangeVal[9])
	simSetUISlider(ui,uiSliderIDs[10],(simGetJointPosition(rightArmJoints[3])-minVal[10])*1000/rangeVal[10])
end 

if (sim_call_type==sim_childscriptcall_cleanup) then 
 
end 

if (sim_call_type==sim_childscriptcall_actuation) then 
	-- Read desired values from the user interface:
	relativeStepSize=minVal[1]+simGetUISlider(ui,uiSliderIDs[1])*rangeVal[1]/1000
	nominalVelocity=minVal[2]+simGetUISlider(ui,uiSliderIDs[2])*rangeVal[2]/1000
	simSetJointTargetPosition(neckJoints[1],minVal[3]+simGetUISlider(ui,uiSliderIDs[3])*rangeVal[3]/1000)
	simSetJointTargetPosition(neckJoints[2],minVal[4]+simGetUISlider(ui,uiSliderIDs[4])*rangeVal[4]/1000)
	simSetJointTargetPosition(leftArmJoints[1],minVal[5]+simGetUISlider(ui,uiSliderIDs[5])*rangeVal[5]/1000)
	simSetJointTargetPosition(leftArmJoints[2],minVal[6]+simGetUISlider(ui,uiSliderIDs[6])*rangeVal[6]/1000)
	simSetJointTargetPosition(leftArmJoints[3],minVal[7]+simGetUISlider(ui,uiSliderIDs[7])*rangeVal[7]/1000)
	simSetJointTargetPosition(rightArmJoints[1],minVal[8]+simGetUISlider(ui,uiSliderIDs[8])*rangeVal[8]/1000)
	simSetJointTargetPosition(rightArmJoints[2],minVal[9]+simGetUISlider(ui,uiSliderIDs[9])*rangeVal[9]/1000)
	simSetJointTargetPosition(rightArmJoints[3],minVal[10]+simGetUISlider(ui,uiSliderIDs[10])*rangeVal[10]/1000)
	
	
	-- Get the desired position and orientation of each foot from the paths (you can also use a table of values for that):
	t=simGetSimulationTimeStep()*nominalVelocity
	dist=dist+t
	lPos=simGetPositionOnPath(lPath,dist/lPathLength)
	lOr=simGetOrientationOnPath(lPath,dist/lPathLength)
	
	p=simGetPathPosition(rPath)
	rPos=simGetPositionOnPath(rPath,(dist+correction)/rPathLength)
	rOr=simGetOrientationOnPath(rPath,(dist+correction)/rPathLength)
	
	
	-- Now we have the desired absolute position and orientation for each foot.
	-- Now transform the absolute position/orientation to position/orientation relative to asimo
	-- Then modulate the movement forward/backward with the desired "step size"
	-- Then transform back into absolute position/orientation:
	astiM=simGetObjectMatrix(asti,-1)
	astiMInverse=simGetInvertedMatrix(astiM)
	
	m=simMultiplyMatrices(astiMInverse,simBuildMatrix(lPos,lOr))
	m[8]=m[8]*relativeStepSize
	m=simMultiplyMatrices(astiM,m)
	lPos={m[4],m[8],m[12]}
	lOr=simGetEulerAnglesFromMatrix(m)
	
	m=simMultiplyMatrices(astiMInverse,simBuildMatrix(rPos,rOr))
	m[8]=m[8]*relativeStepSize	
	m=simMultiplyMatrices(astiM,m)
	rPos={m[4],m[8],m[12]}
	rOr=simGetEulerAnglesFromMatrix(m)
	
	
	-- Finally apply the desired positions/orientations to each foot
	-- We simply apply them to two dummy objects that are then handled
	-- by the IK module to automatically calculate all leg joint desired values
	-- Since the leg joints operate in hybrid mode, the IK calculation results
	-- are then automatically applied as the desired values during dynamics calculation
	simSetObjectPosition(lFoot,-1,lPos)
	simSetObjectOrientation(lFoot,-1,lOr)
	
	simSetObjectPosition(rFoot,-1,rPos)
	simSetObjectOrientation(rFoot,-1,rOr)
	
end 
Другие модели

Вы можете удалить модель — для этого надо её выбрать, и нажать на Del. И можете попробовать посмотреть другие модели в работе, у некоторых есть скрипты для автономной работы.

Мобильные роботы

Стационарные роботы (манипуляторы)

Примеры сцен

Так же есть большое количество примеров (сцен), которые поставляются сразу с программой. Для этого надо выбрать в меню «File/Open scenes» и там перейти в папку: «V-REP3/V-REP_PRO_EDU/scenes».

Вот примеры сцен (файлы с расширением *.ttt):

Файлы сцен-примеров

2IndustrialRobots.ttt
3DoFHolonomicPathPlanning.ttt
6DoFHolonomicPathPlanning.ttt
BarrettHandPickAndPlace.ttt
blobDetectionWithPickAndPlace.ttt
ConstraintSolverExample.ttt
controlTypeExamples.ttt
e-puckDemo.ttt
environmentMapping.ttt
externalIkDemo.ttt
fabricationBlocks.ttt
fastClientServerCommunication.ttt
forwardAndInverseKinematics1.ttt
forwardAndInverseKinematics2.ttt
gearMechanism.ttt
genericDialogDemo.ttt
ghostDemo.ttt
ImageProcessingExample.ttt
inverseKinematicsOf144DofManipulator.ttt
jansenMechanism.ttt
katanaRobotWithCableSimulation.ttt
khepera3.ttt
LineTracer-threaded.ttt
millingMachine.ttt
millingRobot.ttt
motionPlanningAndGraspingDemo.ttt
motionPlanningDemo1.ttt
motionPlanningDemo2.ttt
motionPlanningDemo3.ttt
mouseTestScene.ttt
naturalSelectionAlgo.ttt
NonHolonomicPathPlanning.ttt
objectHandling.ttt
PaintingRobot.ttt
ParallelForwardAndInverseKinematics.ttt
practicalPathPlanningDemo.ttt
proximitySensorDemo.ttt
reflexxesMotionLibraryType4Demo.ttt
robotCollaboration1.ttt
robotCollaboration2.ttt
robotLanguageControl.ttt
rosTopicPublisherAndSubscriber.ttt
SocketAndTubeCommunicationExample.ttt
StripeScanner.ttt
weldingRobot.ttt
wirelessTransmission.ttt
youBotAndHanoiTower.ttt

Ссылки

* основной сайт V-REP
* руководство пользователя (на английском)
* большое количество видео, примеров из V-REP

Чтобы поддержать популяризацию этой интересной системы на русском языке — создана русскоязычная группа по V-REP.

Применение в учебном процессе

На мой взгляд, у V-REP есть хороший потенциал применения в учебных процессах. Если вас интересует применение системы в учебном процессе — в школе, институте, в клубе робототехники и т.п. — то можете заполнить анкету. Может быть получиться консолидировать усилия и сделать учебные русскоязычные материалы.
Планы на будущее


Конечно это лишь малая часть возможностей V-REP системы. В следующей публикации на примере рассмотрим создание задачи гоночного симулятора на робо-машинке от первого лица. Рассмотрим API. Создание объектов, настройка сцены и взаимодействие с пользователем.

Виртуальный Лего-конструктор LeoCAD — ЖУРНАЛ LEGO-УРОК

LeoCAD имеет интуитивно понятный интерфейс, который позволяет новым пользователям начинать создавать новые модели, не тратя слишком много времени на изучение приложения.

 

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

LeoCAD является открытым исходным кодом, поэтому каждый может внести свой вклад в исправления и функции, и он всегда будет оставаться свободным.

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

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

Обратите внимание, что пользовательский интерфейс варьируется между операционными системами, поэтому, если вы не используете версию LeoCAD для Windows, ваше приложение будет немного отличаться от того, что показано на рисунках.

Просмотр списка запасных частей

Начните строить автомобиль, добавив синюю «Пластину 1 x 4»:

Прокрутите список «Детали» (по умолчанию это отображается вдоль правого края рабочей области), пока не найдете группу «Пластина», затем разверните эту группу и выберите «Пластина 1 x 4».

Теперь щелкните по синему прямоугольнику в окне инструментов «Цвета» чуть ниже списка «Parts», и вы заметите, что изображение в правом верхнем углу изменяется, когда вы выбираете разные части или меняете цвета, чтобы отражать ваши текущие варианты.


Добавление части

Добавьте «Пластину 1 x 4» к вашей модели:

Есть несколько способов добавить часть к модели; самый простой – просто перетащить его из списка «Parts» в «Model View». При перетаскивании фрагмента вы увидите предварительный просмотр того, где он будет размещен при перемещении мыши.

Совет. В качестве альтернативы вы можете нажать клавишу «Вставить» на клавиатуре или использовать функцию «Вставить» на панели инструментов.


Перемещение частей

Теперь добавьте синюю «Пластину 2 x 4»:

Выберите «Plate 2 x 4» из списка «Parts» и добавьте его рядом с частью, добавленной ранее. Это должно быть ровно 2 шпильки отдельно от первой части, поэтому, если вы не уронили ее в правильном положении, вам придется ее корректировать сейчас.

Наведите указатель мыши над стрелками над «Пластиной 2 x 4» и обратите внимание, что когда он будет над одним из них, он станет желтым, и указатель мыши изменится на значок с 4 стрелками. Это означает, что если вы теперь удерживаете «Левую кнопку мыши» и перемещаете мышь, текущие выбранные фрагменты будут перемещаться вдоль линии, где находится стрелка.

Совет. Вы также можете использовать клавиши со стрелкой, «вверх» или «вниз» на клавиатуре или «Переместить» на панели инструментов для перемещения фигур.

Совет. Сетка очень полезна, чтобы быстро измерить расстояние между кусками.


Вращающиеся части

Добавьте синюю «Пластину 2 x 8» поверх предыдущих двух частей:

Когда вы сначала перетаскиваете кусок в модель, он будет параллелен первым кускам, но мы хотим, чтобы он был перпендикулярен им, поэтому нам нужно будет вращать его на 90 градусов.

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

Совет. Вы также можете использовать клавиатуру для вращения по всем осям, удерживая нажатой клавишу «Shift», одновременно нажимая стрелки, «Page Up» или «Page Down».


Шаги для строительства

Инструкции по строительству обычно разбиваются на несколько этапов, чтобы люди могли легко понять, как построена модель, поэтому перейдем к «Шагу 2» нашей модели, нажав «Далее» (двойная стрелка вправо) на панели инструментов. Обратите внимание, что текущий шаг отображается на «Строке состояния» в нижнем правом углу главного окна.

Теперь перейдите на панель «Временная шкала» (в списке «Parts»), и вы сможете увидеть шаг, на котором каждая часть была добавлена ​​в модель. Чтобы кто-то еще мог построить наш автомобиль, давайте переместим «Plate 2 x 8» на «Шаг 2», перетащив его на «Сроки» с «Шаг 1» на «Шаг 2».

Если вы вернетесь к «Шагу 1» в инструкциях, нажав «Предыдущие» (двойные левые стрелки), вы увидите, что «Пластина 2 x 8» больше не видна и появляется только на «Шаг 2» или позже ,

Совет. Вы также можете нажать клавиши со стрелками «Влево» и «Вправо», удерживая нажатой клавишу «Alt» на клавиатуре, чтобы перейти между шагами.


Поиск запчастей

Теперь переходите к «Шаг 3» инструкций и добавьте рулевое колесо и брызговики. Если вы не знакомы с соглашением об именах в списке «Parts», может потребоваться некоторое время, чтобы узнать, к какой категории принадлежит эта часть, поэтому давайте использовать функцию «Поиск частей», чтобы найти эти части.

Напечатайте «брызговик» в поле «Поиск частей» и нажмите «Enter», и LeoCAD выполнит поиск всех частей для введенного вами текста и отобразит результаты в поле «Поиск частей» в нижней части списка «Parts».

Выберите «Car Mudguard 2 x 4 без шпилек» и добавьте его в модель, затем сделайте то же самое для рулевого колеса, и у вас будет что-то вроде этого:


Добавление дополнительных частей

Продвиньте инструкции еще на один шаг и добавьте «Наклонный кирпич 45 2 x 2» над брызговиком, а другой – к передней части автомобиля.

Давайте добавим еще одну «Пластину 1 х 4» к машине; вот картина почти полного автомобиля:


Вращение представления

Теперь нам нужно добавить колеса к машине, поэтому давайте начнем с добавления держателей колес в «Шаг 5». Не будет легко увидеть нижнюю часть автомобиля с текущего положения, поэтому давайте повернем камеру, чтобы получить лучший обзор.

Удерживайте нажатой клавишу «Alt» на клавиатуре, когда вы перетаскиваете мышь с помощью кнопки «Левая кнопка мыши», и изображение будет вращаться. При нажатой клавише «Alt» перетаскивание с помощью кнопки «Средняя мышь» панорамирует вид сбоку, а перетаскивание с помощью кнопки «Правая кнопка мыши» будет увеличиваться и уменьшаться.

Как только у вас будет хорошая позиция, добавьте в автомобиль 2 черных «Пластины 2 x 2 с колесиками»:

Совет. Кнопки и клавиши для этих ярлыков можно настроить в меню «Настройки».


Движение

Перейдите к «Шаг 6» и добавьте серый «Колесный ободок 6.4 x 8» и черный «Протектора Tire 6/50 x 8 Offset».

Если вы приближаетесь ближе, вы заметите, что с шагом по умолчанию невозможно получить правильное совмещение колеса с держателем колеса, поэтому вам нужно снять флажок «Переместить привязку», нажав кнопку «Движение» “(Значок левого магнита) на панели инструментов

Одновременно выберите шины и колесо, удерживая клавишу «Control» на клавиатуре и нажимая на каждую деталь, а затем переместите их в правильное положение автомобиля.

Подсказка: текущие приращения движения показаны на «Строке состояния».


Скопировать и вставить

Нам еще нужно еще 3 комплекта колес, но вместо того, чтобы добавлять новые шины и колеса по одному, мы можем сэкономить время, скопировав те, которые мы только что добавили в буфер обмена, и вставим их обратно. Он работает, как и большинство других приложений, – используйте сочетания клавиш или меню «Изменить», чтобы скопировать и вставить в буфер обмена.

Когда вы вставляете копию, она помещается в ту же позицию, что и вы ее скопировали, поэтому нам нужно переместить новые части на другую сторону автомобиля. Поскольку автомобиль имеет ровно четыре шипы, вы можете повторно выбрать «Movement Snap Enabled» и установить его значение «1 Stud», нажав «Snap XY» в меню «Движение привязки», чтобы легко позиционировать новое колесо с другой стороны машина.


Нам еще нужно последнее дело, чтобы закончить проект – водитель! Перейдите на другой шаг и выберите «Minifig Wizard» из меню «Piece», и вы увидите диалог, в котором вы можете настроить мини-фильтр. Выберите цвета и аксессуары, которые вы хотите, и нажмите «ОК», чтобы добавить драйвер.

Готовая машина должна выглядеть примерно так:

Это завершает учебник. Помните, что в этом учебном пособии не рассказывается, как использовать расширенные функции LeoCAD; это всего лишь введение в основные функции.

Не забудьте сохранить файл, если вы хотите вернуться и поиграть с ним позже или отправить его другу. Вы также можете распечатать инструкции или создать LeoCAD веб-страницу с ними для вас – просто изучите все параметры, которые у вас есть в меню. Вы даже можете создавать снимки с трассировкой лучей и использовать их в качестве обоев рабочего стола, если у вас установлен POV-Ray!

Скачать виртуальный конструктор LeoCAD 

Возможно вам это будет интересно!

Автор публикации

не в сети 4 дня

admin

100 Комментарии: 12Публикации: 249Регистрация: 14-06-2017

oop — виртуальная функция C ++ из конструктора

Переполнение стека
  1. Товары
  2. Клиенты
  3. Случаи использования
  1. Переполнение стека Общественные вопросы и ответы
  2. Команды Частные вопросы и ответы для вашей команды
  3. предприятие Частные вопросы и ответы для вашего предприятия
  4. работы Программирование и связанные с ним возможности технической карьеры
  5. Талант Нанять технических талантов
  6. реклама Обратитесь к разработчикам по всему миру
,

c ++ — Когда использовать виртуальные деструкторы?

Переполнение стека
  1. Товары
  2. Клиенты
  3. Случаи использования
  1. Переполнение стека Общественные вопросы и ответы
  2. Команды Частные вопросы и ответы для вашей команды
  3. предприятие Частные вопросы и ответы для вашего предприятия
  4. работы Программирование и связанные с ним возможности технической карьеры
  5. Талант Нанять технических талантов
  6. реклама Обратитесь к разработчикам по всему миру
,

Добавить комментарий

Ваш адрес email не будет опубликован. Обязательные поля помечены *