Как правильно расширить возможности языка программирования используя перегрузку операторов.

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

«А зачем, ведь то, что вы предлагаете, можно сделать имеющимися средствами языка».

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

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

Сегодня я попробую показать почему это так сложно и как правильно использовать перегрузку на примере создания одного нового типа под названием var поведение которого будет максимально приближено к аналогичному типу в JavaScript.

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

Для начала объявим сам класс:

struct var {
};


(Почему struct а не class? Разница между ними лишь в том, что в struct по-умолчанию все члены public. Для упрощения читаемости кода будет struct.)

Попробуем поместить в var числовое значение и строковое:

struct var {
	char *str;
	double num;
};



Теперь надо написать конструкторы. Они вызываются когда вы пишете:

var i = 100;
var s = "hello";

struct var {
	char *str;
	double num;
	var (double initial) {
		num = initial;
	}
	var (char *initial) {
		str = initial;
	}
}


Отлично, теперь, чтобы всё ожило, нам надо вывести значение на экран:

var i = 100, s = "hello";
log(i);
log(s);


Как этого добиться?

void log(var x) {
	....
	а тут то что написать?
}


Как нам узнать, какое из двух содержимых используется в данном экземпляре var?

Ясно, что надо добавить внутренний тип. Но как это сделать? Логично использовать enum:

enum varType { varNum, varStr };


Меняем определение класса:

struct var {
	varType type;
	char *str;
	double num;
	var (double initial); 
	var (char *initial);
};


Теперь в конструкторах надо присвоить тип:

var::var (double initial) {
	type = varNum;
	num = initial;
}
var::var (char *initial) {
	type = varStr;
	str = initial;
}


Ну что же, теперь можно вернуться к log():

void log(var x) {
	if (x.type == varNum) printf("%f\n", x.num);
	if (x.type == varStr) printf("%s\n", x.str);
}


И вот теперь-то нам и надо перекрыть оператор присваивания:

void var::operator = (double initial) {
	type = varNum;
	num = initial;
}

void var::operator = (char *initial) {
	type = varStr;
	str = initial;
}


Теперь можно писать:

var a = 10, b = "hello";


Интересно, что оператор присвоения получился полная копия конструктора. Может быть стоит повторно использовать? Так и сделаем. Везде в «конструкторе присвоения» можно просто вызывать «оператор присвоения».

На данный момент вот наш полный рабочий код:

#include <stdio.h>

enum varType { varNum, varStr };

struct var {
	varType type;
	char *str;
	double num;
	var (double initial); 
	var (char *initial);
	void operator = (double initial);
	void operator = (char *initial);
};

var::var (double initial) {
	(*this) = initial;
}
var::var (char *initial) {
	(*this) = initial;
}

void var::operator = (double initial) {
	type = varNum;
	num = initial;
}

void var::operator = (char *initial) {
	type = varStr;
	str = initial;
}

void log(var x) {
	if (x.type == varNum) printf("%f\n", x.num);
	if (x.type == varStr) printf("%s\n", x.str);
}

int main() {
	var x = 100, s = "hello";
	log(x);
	log(s);
}


А что если мы просто напишем:

int main() {
	var y;
}


Нас обругает компилятор! Мы не можем объявлять переменную не инициализировав её. Непорядок, в чём же дело? А в том, что все наши конструкторы требуют начального значения.

Нам нужен «пустой» конструктор, он же — конструктор по умолчанию, default constructor. Но чему будет равна переменная если она ещё ничему не равна? Ещё неизвестно будет ли она числом или строкой, или чем-то ещё.

Для этого вводится понятие «пустое значение», известное как null или undefined.

enum varType { varNull, varNum, varStr };

var::var() {
	type = varNull;
}


Теперь можно просто объявлять переменные, не думая о типе.

var a, b, c;


И уже в коде присваивать значения:

a = 1; b = "foo";


Но мы ещё не можем написать:

a = b;


Нам нужен оператор присвоения var=var:

void var::operator= (var src) {
	type = src.type;
	num = src.num;
	str = src.str;
}


При присвоении изменится тип! И «а» станет строкой.

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

Сначала нам нужен новый тип в enum:

enum varType { varNull, varNum, varStr, varArr };


Теперь указатель на буффер элементов, и размер:

struct var {
	...
	int size;
	var *arr;
	...
}


Теперь перегрузим оператор доступа к элементу:

struct var {
	...
	var operator [](int i);
	...
}


Такой оператор называется «subscript operator» или оператор индекса.

Наша цель: хранить в массиве элементы типа var. То есть речь идёт о рекурсии.

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

var operator [](char *key);


Нет, так не годится. Нам нужен не указатель на буфер символов, а именно строка, сделаем так:

struct var {
	...
	var operator [](var key);
	...
}


Тогда, когда всё заработает, мы сможем писать:

x[1]


или

x["foo"]


Компилятор преобразует в var! Почему? Ведь у нас уже есть конструкторы из литералов числа и строки.

Можно будет и так писать:

y = "foo";
x[y];


Кстати, литерал, (literal) это «буквальное значение», то есть то значение, которое вы набрали прямо в коде. Например присвоение «int a = b;» это присвоение по имени, а «int a = 123;» это literal assignment, присвоение буквальное, «по литералу» 123.

Одно не понятно, как var становится массивом? Предположим создадим переменную «а», и как сказать, что это массив?

var a ???;


В JavaScript используется несколько способов:

var a = new Array;
var a = [];


Попробуем оба:

var newArray() {
	var R;
	R.type = varArr;
	R.size = 0;
	R.arr = new var [10];
	return R;
}


Пока, для того, чтобы сосредоточится на более существенных вещах, мы сделаем вид, что 10 элементов, это всё, что нам надо.

Теперь интересный момент, попробовать сделать нечто вроде:

var a = [];


Использовать [] в С++ нельзя, но можно использовать любой идентификатор, то есть имя. Например Array.

var a = Array;


Как это сделать? Для этого применим «синтаксический тип», вот так:

enum varSyntax { Array };


Везде где мы упомянем слово «Array», компилятор сообразит, что нужен тип «varSyntax». А ведь менно по типу компилятор выбирает, какую функцию, конструктор или оператор использовать.

struct var {
	...
	var (varSyntax initial) {
		if (initial == Array) {
			type = varArr;
			size = 0;
			arr = new var[10];
		}
	}
	...
}

var a = Array;


Конечно, где конструктор, там и присвоение, сразу вспомним мы, и напишем оператор присвоения по типу varSyntax.

void var::operator=(varSyntax initial) {
	...
}


В нижеследующем коде, сначала «a» инициализируется конструктором var(varSyntax), а затем «b» инициализируется пустым конструктором и присваевается оператором «var operator=(varSyntax)».

var a = Array, b;
b = Array;


Поскольку конструктор и присвоение через "=" всегда ходят парою, логично применить тот же трюк, и в конструкторе повторно использовать код из присвоения.

struct var {
	...
	var (varSyntax initial) {
		(*this) = initial;
	}
	operator= (var Syntax);
	...
};

void var::operator= (varSyntax initial) {
	if (initial == Array) {
		type = varArr;
		size = 0;
		arr = new var*[10];
	}
//	else if (initial == Object) {
//		...
//	}
}


Где-то, там-же, мы сможем создавать пустые объекты. Но это позже.

Ну что, пора попробовать:

int main() {
	var a = Array;
	a[0] = 100;
	log(a[0]);
}


error: conversion from 'int' to 'var' is ambiguous
	a[0] = 100.0;


Ого, ведь вот какая штука, мы объявили operator[] от var. Компилятор почему-то ожидает int. Если поменять var[0] на var[1] то всё скомпилируется. Что такое?

int main() {
	var a = Array;
	a[1] = 100;
	log(a[1]);
}


Так, с единичкой, компилируется…

Только этот код ничего пока не сделает, потому-что мы ещё не написали operator[].

Надо написать! Наверное как-то так:

var var::operator [](var key) {
	return arr[key];
}


error: no viable overloaded operator[] for type 'var *'
        return arr[i];
               ~~~^~


О, компилятор, что ещё не так?

Оказывается индексный доступ к указателю требует int, а как превратить var в int компилятор пока не знает.

Ну можно определить оператор int, есть и такое в C++! Но лучше, где можно не создавать нового оператора, его не создавать (долгая история), поэтому сделаем так:

struct var {
	...
	int toInt() {
		return num;
	}
	...
}

var var::operator[] (var i) {
	return arr[i.toInt()];
}


Компилируется, но ничего не выводит после запуска, в чём же дело?

А как, вообще, оно может работать? Как можно и читать и писать содержимое элемента через один и тот же оператор?

Ведь должны работать обе строчки:

a[1] = 100;
log(a[1]);


В одной запись, в другой чтение. Оказывается, operator= должен возвращать ссылку на элемент. Обратите внимание на символ &, в нём в данном случае дело:

var& var::operator[] (var i) {
	return arr[i.toInt()];
}


Но, хотя, «a[1]» заработало, «a[0]» продолжает ругаться. Почему же всё-таки?

Дело в том, что 0 может считаться и числом и указателем, а у нас var имеет два конструктора, один для числа (double), другой для указателя (char*). Из за этого вроде бы совершенно нормальный код, при использовании 0 в качестве литерала вдруг выдаёт ошибки компиляции. Это одна из особо изощрённых пыток С++ и серии ambiguous call.

Ну а вообще, компилятор в первую очередь считает ноль целым, то есть int.

К счастью, достаточно научить наш var инициализироваться из int. Как обычно сразу пишем конструктор и operator=.

var::var (int initial) {
	(*this) = (double) initial;
}

void var::operator = (int initial) {
	(*this) = (double) initial;
}


Тут, чтобы повторно использовать код, просто перенаправляются оба вызова в operator=(double).

Итак, что получилось на данный момент:

#include <stdio.h>

enum varType { varNull, varNum, varStr, varArr };
enum varSyntax { Array };

struct var {
	varType type;
	char *str;
	double num;
	var ();
	var (double initial); 
	var (int initial); 
	var (char *initial);
	void operator = (double initial);
	void operator = (int initial);
	void operator = (char *initial);

	var *arr;
	int size;
	var &operator [](var i);
	var (varSyntax initial) {
		(*this) = initial;
	}
	void operator= (varSyntax initial);
	void operator= (var src) {
		type = src.type;
		num = src.num;
		str = src.str;
		arr = src.arr;
	}
	int toInt() {
		return num;
	}
};

var::var() {
	type = varNull;	
}
var::var (double initial) {
	(*this) = initial;
}
var::var (int initial) {
	(*this) = (double)initial;
}
var::var (char *initial) {
	(*this) = initial;
}

void var::operator = (double initial) {
	type = varNum;
	num = initial;
}

void var::operator = (int initial) {
	(*this) = (double) initial;
}

void var::operator = (char *initial) {
	type = varStr;
	str = initial;
}

void log(var x) {
	if (x.type == varNum) printf("%f\n", x.num);
	if (x.type == varStr) printf("%s\n", x.str);
}

void var::operator= (varSyntax initial) {
	if (initial == Array) {
		type = varArr;
		size = 0;
		arr = new var[10];
	}
}

var &var::operator[] (var i) {
	return arr[i.toInt()];
}

int main() {
	var x = 100, s = "hello";
	var a = Array;
	a[0] = 200;
	log(a[0]);
	log(x);
	log(s);
}


Кстати, а что если мы захотим вывести массив на экран?

void log(var x) {
	if (x.type == varNum) printf("%f\n", x.num);
	if (x.type == varStr) printf("%s\n", x.str);
	if (x.type == varArr) printf("[Array]\n");
}


Пока только так.

Но хочется же большего.

Во-первых надо сделать самонастраивающуюся длину массива:

var &var::operator[] (var i) {
	int pos = i.toInt();
	if (pos >= size) size = pos+1;
	return arr[pos];
}


И надо сделать push() — добавление одного элемента в конец:

var var::push(var item) {
	if (type != varArr) {
		var nil;
		return nil;
	}
	(*this)[size] = item;
	size++;
	return item;
}


Поскольку мы работаем с указателем, тут не лишне проверить тип. В процессе подготовки этой статьи, как раз на этом программа падала. Ну размер мы пока не проверяем, заняты глобальным проектированием, но мы ещё вернёмся к этому вопросу.

Теперь можно переписать функцию log(), чтобы она выводила массив целиком:

void log(var x) {
	if (x.type == varNum) printf("%f ", x.num);
	if (x.type == varStr) printf("%s ", x.str);
	if (x.type == varArr) {
		printf("[");
		for (int i = 0; i < x.size; i++) log(x[i]);
		printf("]");
	}
}


Какой минимум работы понадобился, что рекурсия животворящая делает!

int main() {
	var a = Array;
	a[0]=100;
	a.push(200);
	log(a[0]);
	log(a[1]);
	log(a);
}


Вывод данных после запуска:

100.000000 200.000000 [100.000000 200.000000]


Ну вот, замечательно, какие-то основы полиморфизма у нас есть.

Можно даже уже помещать массив в массиве, причём вперемешку со строками и числами.

int main() {
	var a = Array;
	a.push(100);
	a.push("foo");
	a[2] = Array;
	a[2][0] = 200;
	a[2][1] = "bar";
	log(a);
}


[100.000000 foo [200.000000 bar ]]


Интересно, что будет если мы попробуем написать так:

var a = Array;
var b = a.push(Array);
b.push(200);
b.push("foo");
log(a);


А вот что:

[[]]


Почему же так получилось?

Проверим таким простым способом:

printf("%\n", a.arr[0].size);
printf("%\n", b.size);


По-логике, мы должны увидеть одно и то же число: 2.

Но на самом деле a.arr[0].size == 0!

Всё дело в том, что a[0] и b это две РАЗНЫЕ переменные, два разных экземпляра. В момент, когда произошло присвоение внутри функции a.push() через return, их поля совпали, то есть size, arr были идентичными, но после b.push() произошло увеличение b.size, и не произошло увеличение a[0].size.

Это мозголомная проблема, которую даже трудно описать словами, и возможно читатель совсем запутался пока читал последние строки, называется «передача по ссылке» (pass by reference).

В С++, обычно, передачей по ссылке называется когда перед аргументом стоит &, но это частный случай. Вообще, это значит, что изменение копии изменяет оригинал.

Посмотрим, как решать такую задачу. Сначала, всё что было связано с массивом, вынесем в отдельный класс, так исторически сложилось, что я его назвал lst. Особо не вдавайтесь в его устройство, так, схватите общую суть:

class lst {
	typedef var** P;
	P p;
	int capacity, size;
	void zeroInit();
	
public:
	lst();
	~lst();
	int length();
	void resize(int newsize);
	var pop();
	void push(const var &a);
	var& operator [](int i);
	void delIns(int pos, int delCount, var *item, int insCount);
};


Поясню, что это маленький класс для хранения списка указателей с возможностью динамически изменять размер, и дополнительными коммандами push/pop/delIns.

Это всё что нам понадобится, для того, чтобы наши массивы близко соответствовали JavaScript Array.

Теперь забудем как был устроен «var» раньше, и попробуем вписать «lst» в него правильно:

struct Ref {
	int uses;
	void *data;
	Ref () {
		uses = 1;
	}
};

struct var {
	varType type;
	union {
		double num;
		Ref* ref;
	};
...
};


Во-первых, мы совместили num и ref, потому-что всё равно одновременно нам эти свойства не нужны. Память экономим.

Во-вторых, вместо прямого значения всего, что связано с массивом, у нас будет ссылка со счётчиком внутри. Это называется подсчёт ссылок или reference counting.

В такой-же ссылке будем потом хранить Object.

Заметим, что счётчик сразу устанавливается в 1.

Всегда, когда программируется подсчёт ссылок, сразу пишется два основных метода, «присоединитель» и «отсоединитель».

В первом делается «ref=src.ref, ref->uses++», обычно он называется copy, link, attach, или, собственно, reference.

void var::copy(const var &a) {
	// к этому месту экземпляр должен быть пуст.
	type = a.type;
	if (type == varNum || type == varBool) num = a.num;
	else {
		if (a.type == varNull) { return; }
		ref = a.ref;
		if (ref) ref->uses++;
	}
}


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

Обычно он называется unlink, unreference, detach. Я привык его называть unref().

void var::unref() {
	if (type == varNum || type == varNull || type == varBool) return;
	else if (type == varStr) {
		ref->uses--;
		if (ref->uses == 0) {
			delete (chr*)ref->data, delete ref;
		}
	}
	else if (type == varArr) {
		ref->uses--;
		if (ref->uses == 0) {
			deleteLst();
		}
	}
	else if (type == varObj) {
		ref->uses--;
		if (ref->uses == 0) {
			deleteObj();
		}
	}
	type = varNull;
	ref = 0;
}


data в структуре Ref имеет тип void*, то есть просто указатель, и будет хранить ссылку на собственно экземпляр массива(lst) или объекта(obj). При словe объект, речь о том объекте, в котором мы будет хранить пары ключ/значение в соответствии с JavaScript [Object object].

В сущности подсчёт ссылок, это разновидность сборщика мусора.

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

И, как видите, он не так уж прост, мозг сломать можно на раз.

Просто, чтобы читатель не запутался, повторю всё сначала:

Делаем класс var и в нём инкапуслируем либо double, либо lst (для массива), либо chr(для строк), либо keyval(для объектов).

Вот наш класс для работы со строками:

struct chr {
	int size;
	wchar_t *s;
	chr ();
	~chr();
	void set(double i);
	void set(wchar_t *a, int length = -1);
	void setUtf(char *a, int length = -1);
	void setAscii(char *a, int length = -1);
	char * getAscii();
	char * getUtf();

	wchar_t operator [](int i);

	double toNumber ();
	int intToStr(int i, char *s);
	void dblToStr (double d, char *s);

	int cmp (const chr &other);
	int find (int start, wchar_t *c, int subsize);
	chr substr(int pos, int count = -1);
	int _strcount(const chr &substring);
	void _cpto(int from, const chr &dest, int to, int count);
	chr clone();
	void replace(chr &A, chr &B, chr &dest);
};


И вот класс для объектов:

struct keyval {
	var keys, vals;
	keyval ();
	void set(var key, var val);
	var &get(var key);
};


Тут уже полная рекурсия и полиморфизм, смотрите, keyval использует массивы в виде var. Чтобы самому стать частью var. И это работает!

Одна важнейшая особенность использования подсчёта ссылок в том, что если вы хотите изменить объект, вы должны понимать, что все кто на него ссылаются тоже получат изменённый объект.

Например:

void f(var t) {
	t += "world";
	log(t);
}

var s = "hello";
f(s);
log(s);


Вывод:

world
world


При передаче s в f() вместо копирвания всех символов строки, только копируется один указатель и увеличивается один счётчик.

Но после изменения строки t так же изменится и строка s. Что нам надо в случае массивов, но не надо в случае строк! Это называется передача по ссылке, pass-by-reference.

Когда нам надо, чтобы переменная переданая через подсчёт ссылок была изменена отдельно от своего исходника, мы должны перед каждым изменением вызывать функцию detach/unref/unlink.

Так работают строки в Delphi, например. Это называется термином copy-on-write.

Считается, что это плохое решение. Но как отказаться от copy-on-write, но сохранить воможность pass-by-reference и copy-pointer-and-increment (подсчёт ссылок)?

Ответ стал стандартом современного программирования: а не надо изменять переменную, сделайте её неизменной! Это называется immutability — неизменяемость.

Согласно принципу неизменяемости строки в JavaScript устанавливаются только один раз, и после этого их изменять нельзя. Все функции работы со строками которые что-то изменяют, вызвращают новые строки. Это сильно облегчает нелёгкий труд по тщательной расстановке всех copy/unref, проверок указателей и прочей работе с памятью.

Тут, внезапно, я вынужден прерваться, ведь статья превысила комфортные для читателя 20К символов. А ведь ещё надо перегрузить около 20 операторов! Даже operator, (запятая). Совместить объекты и массивы, написать JSON.parse, реализовать сравнения строк и булевых значений, написать конструктор для Boolean, придумать и реализовать нотацию для инициализации значений массивов и объектов, решить проблему многоаргументного log(...), придумать что делать с undefined, typeof, правильно реализовать replace/slice и т.д. И всё это без единого шаблона, только перегрузка операторов и функции.

Так что, если вам будет интересно, то скоро продолжим.

Для самых любопытных, ссылка на репозиторий библиотеки:

github.com/exebook/jslike

Комментарии (31)


  1. AlexPublic
    29.06.2015 07:29
    +3

    И снова в статье вроде как про C++ представлен на самом деле «C с классами»… Или точнее в данном случае «C с перегрузкой операторов». )


    1. Chaos_Optima
      29.06.2015 12:10

      Угу а если будем писать на С++ с шаблонами получим С с шаблонами да? Так можно что угодно вывернуть. Где же тогда тот самый С++ по вашему мнению, который не С с чем-то?


      1. AlexPublic
        29.06.2015 16:56
        +5

        Для начала, если мы пытаемся писать на C++, а не на C, то для строк у нас будет wstring, а не wchar_t *, для массивов vector или array (по ситуации), а не var* и для подсчёта ссылок (кстати про это ещё ниже) shared_ptr. Собственно ручная работа с памятью в прикладном коде на C++ — это практически нонсенс, если не считать пары особых случаев, но тут явно не он. Это вот самые базовые вещи, но даже они тут нарушены. Я уже не говорю о более тонких вещах, типа конструкторов перемещения и т.п.

        Ну и наконец насчёт архитектуры в принципе. Вообще идея демонстрировать что-то на C++ реализуя копию поведения JS крайне сомнительна, т.к. тут наблюдается полное противоречие базовых принципов. Медленные ссылочные типы (да ещё и на базе подсчёта ссылок!) очень плохо ложатся на современный C++, базирующийся на семантике перемещения. А вот в том же C это вполне используемый инструмент (за отсутствием других автоматических средств).

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

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


        1. Chaos_Optima
          29.06.2015 17:25
          -1

          То есть для вас признаком С++ является использование стандартной библиотеки? А если её не используют то это просто С с классами, ссылками, перегрузкой операторов и RAII так (в контексте данной статьи)? Мне почему то кажется, что это не совсем верный подход.


          1. AlexPublic
            29.06.2015 18:20
            +2

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

            Кстати, в данном коде велосипеды ещё и кривые. Например подсчёт ссылок реализован не потокобезопасно (а ну да, atomic — это же тоже C++...) и т.д. и т.п.

            Ну и надо определиться. Мы тут делаем точную копию JS или не обязательно? В первом случае где тогда замыкания? А во втором случае почему используются не типы значения (и тогда соответственно RAII и семантика перемещения), традиционные для современного C++, а неудобные ссылочные типы (да ещё и с такой реализацией)?


            1. Chaos_Optima
              29.06.2015 21:13

              Причём здесь велосипеды? Я ни в коем случае не защищаю код автора статьи (имхо он посредственен, и да я тоже думаю, что нужно использовать средства библиотеки вместо велосипедов) но тем не менее это С++, а не С с классами или операторами. Просто каждый раз как кто-то такое пишет, так и хочется узнать что же тогда для человека С++, сколько фичь нужно использовать из набора С++, чтобы из С с чем то, код превратился в С++. В вашем случае как я понимаю нужно всего лишь использовать STL.


              1. AlexPublic
                29.06.2015 21:25

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


                1. Chaos_Optima
                  29.06.2015 22:06

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


                  1. AlexPublic
                    29.06.2015 22:44
                    +1

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


                  1. khim
                    30.06.2015 00:07

                    По-моему оверинжиниринг — это скорее как раз вот это:

                    #define constructor
                    …
                    constructor var () {
                    
                    Я понимаю, что этим даже «отцы-основатели» баловались (хорошо известный пример из оригинального Bourne Shell'а):
                    #define BEGIN	{
                    #define END	}
                    #define SWITCH	switch(
                    #define IN	){
                    #define ENDSW	}
                    #define FOR	for(
                    #define WHILE	while(
                    #define DO	){
                    #define OD	;}
                    #define REP	do{
                    #define PER	}while(
                    #define DONE	);
                    #define LOOP	for(;;){
                    #define POOL	}
                    Но в современном мире такие вещи вытворять не принято. И уж тем более не принято разносить реализацию одного класса по разным файлам с помощью #include (иногда приходится, да, но обычно всё-так стараются так не делать). Да и вообще количество WTFов при чтении этого творения зашкаливает.

                    Ошибки, выдающиеся в strdout — это ведь такой идеоматичный C++, правда?

                    P.S. Вообще же, что бы распарсить JSON на C++ ничего этого не нужно. Возьмите json_parser и не мучайтесь.


                    1. Chaos_Optima
                      30.06.2015 00:12

                      Повторюсь, не я топикастер, код не мой. И это не оверинжиниринг, а просто глупость.


          1. khim
            29.06.2015 19:22
            +1

            Признаком C++ является вменяемый код, в первую очередь. Где для UTF-16 используется не четырёхбайтный wchar_t, а двухбайтовый char16_t, где не изобретается без нужды велосипед (что заодно позволяет не порождать ошибок вида if (size < size) return -1; ), где грамотно используется const и права доступа (что будет со всеми вашими построениями, когда кто-то в ваши структуры ручками залезет и там всё испортит?).

            И да, имея

              jslike::var a;
              std::string b;
            
            хочется-таки писать
              a = b;
            
            а не
            a = static_cast<char *>(b.c_str());
            

            В конце-концов всё, что вы тут наворотили — это синтаксический сахар, а зачем он нужен, если при его использования у меня изо всех щелей будут лезть c_str и static_castы?

            Почему у вас operator + не через operator += реализован? В результате если у вас есть
              jslike::var a = 2;
              jslike::var b = "test";
            
            то
              a += b;
            
            даёт undefined притом что
              a = a + b;
            
            даёт 2test что, как по мне, ну… несколько странно.

            Может быть не стоит писать статью «по мотивам» создания сырой и недоработанной библиотеки, а?


            1. Chaos_Optima
              29.06.2015 21:16

              Признаком C++ является вменяемый код, в первую очередь.
              А невменяемый код тогда на чём написан? ))
              В конце-концов всё, что вы тут наворотили — это синтаксический сахар, а зачем он нужен, если при его использования у меня изо всех щелей будут лезть c_str и static_castы?

              Не забывайте топикастер не я, и я не защищаю код, моё замечание было совершенно по другому поводу.


              1. khim
                29.06.2015 23:31
                +1

                А невменяемый код тогда на чём написан? ))
                А это уже криминалист нужен. Хорошо известно, что целеустремлённый Настоящий Программист может писать фортрановские программы на любом языке, но отсюда не следует, что всё, что не написано на C++ написано на фортране.


            1. Hertz
              30.06.2015 00:47

              sizeof(wchar_t) implementation defined.


              1. khim
                30.06.2015 00:53

                Я собственно как раз об этом. В моём конкретном случае — он четырёхбайтовый, а используется он для UTF-16 строк. Для этих целей в C++ есть char16_t.


  1. zagayevskiy
    29.06.2015 07:43

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

    В С++ вместо log() есть замечательный оператор <<. По-моему, это выглядит примерно так:

    using namespace std;
    class var{
        //...
        friend ostream& operator<<(ostream&, const var&);
        //...
    }
    ostream& operator<<(ostream& out, const var& value){
        out<<value.something;
        return out;
    }
    //...
    var v;
    cout<<"My var:"<<v<<endl;
    


  1. AxisPod
    29.06.2015 08:01
    +9

    Это что вообще я только что прочитал? Во первых зачем тут вообще JavaScript в заголовке, во вторых стоит самому нормально выучить, а уже затем пытаться учить других. И очередное изобретение класса строки, ну это вообще моветон.


  1. MuLLtiQ
    29.06.2015 10:44
    +4

    Это скорее «Приключения C++ в мире JavaScript»
    Ну и статья определенно кандидат в хаб «Ненормальное программирование» :)


  1. roman_kashitsyn
    29.06.2015 10:50
    +5

    Ух… если кому-то нужно похожее, но production quality — Folly/Dynamic.


    1. NeoCode
      30.06.2015 15:52

      Любопытная библиотека, спасибо!


  1. maaGames
    29.06.2015 16:06
    +1

    Какие только велосипеды не изобретают люди, лишь бы не компилировать boost…

    <code class="cpp">
    
    var s1 = new char[10]; // Здравствуйте, утечки памяти
    
    char a[2] = {'a','b'};
    var s2 = a; // Здравствуй, "харт блидинг"
    
    </code><pre>
    


    1. roman_kashitsyn
      29.06.2015 17:03
      +1

      лишь бы не компилировать boost

      О какой конкретно библиотеке из буста идёт речь? Boost.Variant, к примеру, не подходит в качестве замены folly::dynamic (иначе бы dynamic не стали писать).

      Ваши примеры кода не репрезентативны и представляют из себя типичный ССЗБ.

      // Утечка памяти, виноват std::string!!!1
      std::string heap_garbage(new char[10]);
      
      char a[2] = {'a', 'b'};
      // Читаем стек до первого нуля, виноват std::string!!!1
      std::string stack_garbage(a);
      


      Код автора ужасен во многих отношениях, но конкретно ваши претензии не обоснованы.


      1. khim
        29.06.2015 19:29

        Первый пример обоснован по отношению к тому, что описано в статье (в библитеке на github'е конкретно этого ужаса нет, но есть много другого… интересного). Обратите внимание на то, что в этом чуде данные переданные как char * никуда не копируются. Так что без new вы туда char[10] передать не сможете. А вот new char[10] передать можно — но тогда да, память начнёт утекать.


        1. roman_kashitsyn
          29.06.2015 20:43

          без new вы туда char[10] передать не сможете

          Почему? Передать можно, но если объект var выйдет из области видимости раньше соответствующего char[10], то будет UB. В противном случае должно «работать».

          Просто хочется точности — проблема не в утечках памяти, а в том, что объект var в первоначальном определении не владеет данными, на которые ссылается, что ведёт к потенциальной возможности висячих указателей и, соответственно, UB.


      1. maaGames
        29.06.2015 19:55

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


  1. nickolaym
    29.06.2015 19:29
    +6

    «Как я перестал бояться и полюбил атомную верёвку для стрельбы в ногу».
    По-моему, статья будет вредна для любого, кто начал изучать С++, ибо учит антипаттернам и велосипедам.
    Сплошные шаги по граблям, буквально с первых строк.

    Если хочется писать на С++ «с нуля», то придётся изучить управление памятью и временем жизни объектов.
    Для яваскриптщика это определённый порог входа.
    Если хочется сразу перейти к сути, то не надо издеваться над собой и читателями, используя голые типы char* и var*. Тем более, НЕПРАВИЛЬНО их используя.
    Взяли бы std::string и std::vector, и было бы счастье.

    За одно лишь «копирование посредством присваивания» надо выдать чугунную медаль.
    Да, в гитхабе код выглядит свободным от ошибок (хотя я не вчитывался тщательно).
    Но ведь статью читают дети! И они научатся! Плохому научатся, Карл!!!


  1. Hertz
    29.06.2015 23:26
    +3

    В современном C/C++ "" строковые литералы имеют тип const char[N], так что код с char* не const-корректен. А ещё у конструкторов есть member initializer list, и стоило бы инициализации делать там, а не присваивание в теле конструктора.
    И вообще ваш var, это union, tagged union, нет смысла в layout'е выделять память под все возможные хранимые типы.


    1. khim
      29.06.2015 23:37

      И вообще ваш var, это union, tagged union, нет смысла в layout'е выделять память под все возможные хранимые типы.
      Угу. И специально для этого в C++ можно описывать union'ы без имени. В версии на gihub'е это есть. Конечно это «чудесный» union с двумя полями — один int, другой data (а дальше — куча cast'ов), что, конечно, тоже непорядок (лучше было бы перечислить все типы с которыми реально происходит работа).


  1. alexeiz
    30.06.2015 08:36
    +3

    Реализация конструкторов через присваивание; operator=, возвращающий void… Кому-то было бы не плохо повторить основы C++.


  1. qRoC
    30.06.2015 17:33

    Советую посмотреть на простую, но мощную часть библиотеки folly — dynamic