最大オーバーロード-C ++の世界でのJavaScriptの冒険

演算子のオーバーロードを使用してプログラミング言語の機能を適切に拡張する方法。



プログラミング言語の作成者および保守者は、多くの場合、言語に新しい機能を追加するように求められます。 彼らから聞くことができる最も一般的な答えは次のとおりです。



「なぜ、あなたが提供するものは、言語の手段で利用可能にできるからです。」



演算子のオーバーロードは、自家製のデータ型、多数、行列を使用して便利に操作したい物理学者や数学者の要求により、C ++に登場しました。



物理学者と数学者はこの機能を好んでいましたが、C ++クリエーターを含むプログラマーは、演算子のオーバーロードを本当に好むことはありませんでした。 複雑すぎて、多くの暗黙の事柄であるため、オペレーターの過負荷は、まれなケースで使用される有害なものの意見を定着させました。



今日は、その動作がJavaScriptの同様の型に可能な限り近いvarという名前の新しい型を1つ作成することで、なぜそれが難しいのか、オーバーロードを正しく使用する方法を示します。



つまり、数値、文字列、配列、またはオブジェクトのいずれかを含むことができるクラスを作成しようとします。 言語リテラルで初期化できるタイプ。 必要に応じて正しく変換される型。



最初に、クラス自体を宣言します。



struct var { };
      
      







(なぜクラスではなく構造体なのか?唯一の違いは、構造体ではデフォルトですべてのメンバーがパブリックであるということです。コードを読みやすくするために構造体があります。)



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) { ....     ? }
      
      







2つのコンテンツのどちらが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; }
      
      







さて、今、あなたはログに戻ることができます():



 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; }
      
      







私たちはコンパイラに虐待されています! 初期化せずに変数を宣言することはできません。 混乱、問題は何ですか? そして、すべてのデザイナーが初期値を必要とするという事実。



「空の」コンストラクターが必要です。これは、デフォルトコンストラクター、デフォルトコンストラクターでもあります。 しかし、変数が他のものと等しくない場合、変数は等しくなりますか? それが数字なのか文字列なのか、それとも何かなのかはまだ不明です。



これを行うために、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; }
      
      







割り当てられると、タイプが変更されます! そして、「a」は文字列になります。



先に進みましょう。 数字と線が未完成であることを一時的に忘れてください。 配列を処理してみましょう。



まず、enumに新しい型が必要です。



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







次に、要素バッファへのポインタとサイズ:



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







次に、要素アクセス演算子をオーバーロードします。



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







このような演算子は、「添字演算子」またはインデックス演算子と呼ばれます。



目標:var型の要素を配列に格納すること。 つまり、再帰について話しているのです。



ところで、同じ演算子を使用して、文字列内の個々の文字とオブジェクトのプロパティにアクセスする必要があります。 しかし、オブジェクトの場合、入力に線があります。 結局のところ、キーは文字列値です。



 var operator [](char *key);
      
      







いいえ、それは良くありません。 文字バッファへのポインタは必要ありませんが、文字列が必要です。これをやってみましょう。



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







次に、すべてが機能したら、次のように記述できます。



 x[1]
      
      







または



 x["foo"]
      
      







コンパイラーはvar! なんで? 結局のところ、数値と文字列のリテラルからのコンストラクターが既にあります。



次のように書くことができます:



 y = "foo"; x[y];
      
      







ところで、リテラル(リテラル)は「リテラル値」、つまり、コードに直接入力した値です。 たとえば、割り当て「int a = b;」は名前による割り当てであり、「int a = 123;」はリテラル割り当て、リテラル割り当て、「by literal」123です。



1つのことは明らかではありません。varはどのように配列になりますか? 変数「a」を作成し、それが配列であると言うにはどうすればよいでしょうか?



 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 = [];
      
      







C ++では[]を使用できませんが、任意の識別子、つまり名前を使用できます。 たとえば、配列。



 var a = Array;
      
      







どうやってやるの? これを行うには、次のように「構文タイプ」を適用します。



 enum varSyntax { 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;
      
      







うわー、それは私たちがvarから演算子[]を宣言したことです。 何らかの理由で、コンパイラはintを予期しています。 var [0]をvar [1]に変更すると、すべてがコンパイルされます。 なに



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







だから、1つで、それはコンパイルされます...



まだ演算子[]を書いていないので、このコードだけはまだ何もしません。



書かなければならない! おそらくこのようなもの:



 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には2つのコンストラクターがあり、1つは数値(double)用、もう1つはポインター(char *)用です。 このため、リテラルとして0を使用すると、完全に正常なコードのように見え、突然コンパイルエラーが発生します。 これは、C ++とあいまいな呼び出しシリーズの最も洗練された拷問の1つです。



しかし、一般的に、コンパイラはまず整数ゼロ、つまりintを考慮します。



幸いなことに、varをintから初期化するように教えるだけで十分です。 いつものように、すぐにコンストラクタと演算子=を書きます。



 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]; }
      
      







そして、あなたはプッシュ()を行う必要があります-最後に1つの要素を追加します:



 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が2つの異なる変数、2つの異なるインスタンスであるということです。 リターンを介して関数内でa.push()が割り当てられた時点で、それらのフィールドは一致しました。つまり、サイズ、arrは同じでしたが、b.push()の後、b.sizeの増加とa [0]の増加はありませんでした。サイズ。



これは脳の問題であり、言葉で説明することさえ難しく、おそらく「参照渡し」と呼ばれる最後の行を読んでいる間、読者は完全に混乱しています。



C ++では、&が引数の前にある場合、通常、参照渡しは呼び出されますが、これは特別な場合です。 一般に、これは、コピーを変更するとオリジナルが変更されることを意味します。



そのような問題を解決する方法を見てみましょう。 最初は、配列に接続されていたすべてのものが別のクラスに入れられたため、歴史的に起こり、私はそれを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配列と厳密に一致するようにするために必要なことはこれだけです。



さて、前に「var」がどのように配置されたかを忘れて、「lst」を正しく入力してみてください。



 struct Ref { int uses; void *data; Ref () { uses = 1; } }; struct var { varType type; union { double num; Ref* ref; }; ... };
      
      







まず、numとrefを組み合わせました。すべて同じであると同時に、これらのプロパティは必要ないからです。 メモリを節約します。



第二に、配列に接続されているすべてのものの直接的な値の代わりに、内部にカウンターとのリンクがあります。 これは参照カウントと呼ばれます。



同じリンクに、オブジェクトを保存します。



カウンタはすぐに1に設定されることに注意してください。



参照カウントがプログラムされるたびに、「コネクタ」と「ディスコネクタ」という2つの主要な方法がすぐに記述されます。



最初は「ref = src.ref、ref->は++を使用」で、通常はコピー、リンク、アタッチ、または実際には参照と呼ばれます。



 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++; } }
      
      







次に、逆のプロセスが発生し、使用カウンタが減少し、ゼロになった場合、元のメモリが解放されます。



通常、リンク解除、参照解除、切り離しと呼ばれます。 以前は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; }
      
      







Ref構造体のデータはvoid *型、つまり単なるポインタであり、配列(lst)またはオブジェクト(obj)の実際のインスタンスへのリンクを格納します。 オブジェクトという単語では、JavaScript [オブジェクトオブジェクト]に従ってキー/値のペアを格納するオブジェクトについて説明しています。



基本的に参照カウントは、ガベージコレクターの一種です。



通常、「ガベージコレクター」(GC)という言葉はタイマーで実行されるインターバルコレクターを意味しますが、技術的には、Wikipediaの分類によると、リンクカウントは最も単純なガベージコレクターです。



そして、あなたが見ることができるように、それはそれほど単純ではありません、脳は一度に壊れることができます。



読者が混乱しないように、もう一度繰り返します。



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の一部になるため。 そしてそれは動作します!



参照カウントを使用する最も重要な機能の1つは、オブジェクトを変更する場合、それを参照するすべてのユーザーも変更されたオブジェクトを受け取ることを理解する必要があることです。



例:



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







結論:



 world world
      
      







文字列内のすべての文字をコピーする代わりにsをf()に渡すと、1つのポインターのみがコピーされ、1つのカウンターがインクリメントされます。



ただし、文字列tを変更すると、文字列sも変更されます。 配列の場合に必要なものですが、文字列の場合には必要ありません! これは、参照渡しと呼ばれます。



参照カウントを介して渡される変数をそのソースとは別に変更する必要がある場合、各変更の前にdetach / unref / unlink関数を呼び出す必要があります。



これは、たとえば、Delphiで文字列が機能する方法です。 これは、コピーオンライトという用語と呼ばれます。



これは悪い決定であると考えられています。 しかし、コピーオンライトを拒否する方法はありますが、参照渡しとコピーポインタとインクリメントを可能に保つには(参照カウント)?



答えは現代のプログラミングの標準となっています。変数を変更する代わりに、それを不変にします! これは不変性と呼ばれます。



不変性の原則によれば、JavaScriptの行は一度だけ設定され、その後は変更できません。 何かを変更するストリングを操作するすべての機能は、改行を返します。 これにより、すべてのcopy / unrefs、ポインタチェック、およびメモリを使用するその他の作業を慎重に配置するという困難な作業が大幅に容易になります。



ここで、突然、記事が読者にとって快適な20K文字を超えたため、中断しなければなりません。 しかし、まだ約20人のオペレーターをリロードする必要があります! 偶数演算子(コンマ)。 オブジェクトと配列を組み合わせ、JSON.parseを記述し、文字列とブール値の比較を実装し、ブールのコンストラクターを記述し、配列とオブジェクトの値を初期化する表記法を考え出し、実装し、複数引数ログの問題を解決します(...)、未定義、typeof交換/スライスなどを正しく実装します。 そして、これはすべて単一のテンプレートなしで、演算子と関数のオーバーロードのみです。



そのため、興味がある場合は、すぐに継続します。



最も興味深いのは、ライブラリリポジトリへのリンクです:



github.com/exebook/jslike



All Articles