C ++でコーディングするときにポインターが推奨されないのはなぜですか?


45

どこかから、C ++を使用するときは、ポインターを使用しないことをお勧めします。C ++を使用しているときに、ポインターがなぜそんなに悪い考えなのか。ポインターの使用に慣れているCプログラマーにとって、C ++のより良い代替手段とアプローチは何ですか?


40
「どこか」にリンクしてください。コンテキストは非常に関連性があります。

1
この質問はあなたにとって役に立つことを願っています。
ガレットクラボーン

これらの回答のほとんどは、主な理由としてメモリリークを回避することに言及しています。ポインターを使用しているにもかかわらず、アプリの1つでメモリリークの問題が最後に発生したことを思い出せません。メモリリークの問題がある場合は、適切なツールを使用していないか、何をしているのかわかりません。ほとんどの開発環境には、組み込みのリークを自動的にチェックする方法があります。メモリリークの問題は、ガベージコレクション言語では発生がはるかに微妙であり、犯人を追跡するサードパーティツールが必要になることが多いため、追跡がはるかに難しいと思います。
ダンク

1
@Dunkのコメントに追加すると、高レベル言語の組み込みのガベージコレクターが正しく機能しないことがあります。たとえば、ActionScript 3のガベージコレクターはサポートしていません。現在NetConnection、サーバーからのインスタンスの切断(stackoverflow.com/questions/14780456/…)に関係するバグがあります。また、プログラム内に複数のオブジェクトがあり、特に収集を拒否するという問題があります。 ...
パンツァークライシス

...(adobe.com/devnet/actionscript/learning/as3-fundamentals/…-でGCRoots are never garbage collected.始まる段落を検索The MMgc is considered a conservative collector for mark/sweep.)。技術的には、これはAS3自体ではなくAdobe Virtual Machine 2の問題ですが、ガベージコレクションが本質的に組み込まれている高レベル言語でこのような問題がある場合、言語内でデバッグするための本当の方法がないことがよくありますこれらの問題は完全にプログラム外です。...
パンツァークライシス

回答:


58

通常のポインターの代わりにスマートポインターを使用する必要があることを意味すると思います。

コンピューターサイエンスでは、スマートポインターは、自動ガベージコレクションや境界チェックなどの追加機能を提供しながら、ポインターをシミュレートする抽象データ型です。これらの追加機能は、効率を維持しながら、ポインターの誤用によって引き起こされるバグを減らすことを目的としています。スマートポインターは通常、メモリ管理の目的で、それらが指すオブジェクトを追跡します。

ポインターの誤用はバグの主な原因です。ポインターを使用して作成されたプログラムで実行する必要がある定数の割り当て、割り当て解除、および参照は、メモリリークが発生するリスクをもたらします。スマートポインターは、リソースの割り当て解除を自動化することにより、メモリリークを防止しようとします。オブジェクトへのポインター(または一連のポインターの最後)が破棄されると、たとえば、オブジェクトがスコープ外になると、ポイントされたオブジェクトも破棄されます。

C ++では、ガベージコレクションとメモリリークの防止に重点が置かれます(2つだけを挙げます)。ポインターは言語の基本的な部分です。したがって、ポインターを使用しないことは、最も重要なプログラムを除き、ほとんど不可能です。


22
通常、古典的な意味での厳密なガベージコレクションではなく、参照カウントが増えます。少なくとも、私が慣れているスマートptrでは([boost | std] :: shared_ptr)
ダグT.

3
この答えはスマートポインターに非常に限定されていますが、これは問題の小さな側面にすぎません。また、スマートポインターはガベージコレクションではありません。
-deadalnix

3
また、一般的にRAII。
クライム

3
いいえ、これは意味ではありません。それは一つの側面にすぎず、最も重要な側面ではありません。
コンラッドルドルフ

スマートポインターは最も重要な側面だと思いますが、他にもたくさんあることに同意します。
DeadMG

97

私は論争的な「f * ckingポインターを使用しないでください」を発表した人なので、ここでコメントする必要があると感じています。

まず第一に、論争としてそれは明らかに極端な視点を表します。あります(生)のポインタの合法的な用途は、間違いなく。しかし、私(および多くのプロのC ++プログラマー)は、これらのケースは非常にまれだと主張しています。しかし、私たちが本当に意味することは次のとおりです。

最初:

生のポインタがメモリを所有してはいけません。

ここで、「独自のメモリ」とは、本質的に、ある時点deleteでそのポインタで呼び出されることを意味します(ただし、それよりも一般的です)。このステートメントは、絶対的に安全に解釈できます。唯一、独自のスマートポインタ(または他のメモリ管理戦略)を実装する場合は例外です。そこでも、通常は低レベルでスマートポインターを使用する必要があります。

この理由は非常に簡単です。メモリを所有する生のポインタはエラーの原因となります。そして、これらのエラーは、既存のソフトウェアで大量に発生します。メモリリークと二重削除-どちらも、リソースの所有権が不明確な直接の結果です(ただし、反対方向に進みます)。

この問題は、生のポインタの代わりにスマートポインタを使用するだけで、実質的に無料で完全に解消できます(注意:もちろん、これはまだ考える必要があります;共有ポインタサイクルにつながる可能性があり、メモリリークになりますが、これは簡単です)回避可能)。

第二:

C ++でのポインターのほとんどの使用は不要です。

他の言語とは異なり、C ++は値のセマンティクスを非常に強力にサポートしており、ポインターの間接指定を必要としません。これはすぐには実現しませんでした-歴史的に、C ++はCでのオブジェクト指向を容易にするために発明され、ポインターで接続されたオブジェクトグラフの構築に大きく依存していました。しかし、現代のC ++では、このパラダイムが最適な選択となることはめったになく、現代のC ++のイディオムは、多くの場合、ポインターをまったく必要としません。ポインタではなくを操作します

残念ながら、このメッセージはまだC ++ユーザーコミュニティの大部分で受け入れられていません。その結果、作成されるC ++コードのほとんどには、コードが複雑で、遅く、欠陥/信頼性のない余分なポインタが散らばっています。

現代のC ++を知っている人にとってポインターが必要になることはほとんどないことは明らかです(スマートまたはraw。イテレーターとして使用する場合を除く)。結果のコードは、短く、複雑でなく、読みやすく、多くの場合、より効率的で信頼性が高くなります。


5
ため息...これは本当に30以上の賛成票での答えになるはずです...特に2番目の点については。最新のC ++では、ポインターが必要になることはほとんどありません。
チャールズサルビア

1
たとえば Guiオブジェクトは、多数のdocオブジェクトを所有しています。ポインタとしてこれらを持っているので、クラスを前方宣言することができ(再コンパイルを回避します)、オブジェクトを空の状態でスタックに作成して後で提出するのではなく、作成時に(新規で)初期化できますか?これはポインタの完全に有効な使用のようです-すべてのカプセル化されたオブジェクトがヒープ上にあるべきではありませんか?
マーティンベケット

4
@Martin GUIオブジェクトは、ポインターオブジェクトグラフが実際に最良のソリューションである1つのケースです。しかし、メモリを所有する生のポインタに対する命令は依然として有効です。全体で共有ポインタを使用するか、適切な所有権モデルを開発し、生のポインタを介してコントロール間に弱い(=非所有)関係のみを設定します。
コンラッドルドルフ

1
VF1 @ std::unique_ptr。また、なぜptr_vecですか?ただし、通常は、値ベクトルは依然として高速にスワップされます(特に移動セマンティクスの場合)。
コンラッドルドルフ

1
@gaazkam標準コンテナのみを使用することを強制する人はいません。たとえば、Boostには、不完全な型をサポートするマップ実装があります。別の解決策は、型消去を使用することです。boost::variantwith recursive_wrapperはおそらくDAGを表す私のお気に入りのソリューションです。
コンラッドルドルフ

15

生メモリへのアクセスや割り当て後のクリーンアップなど、ポインターを使用することの気質的な側面を隠す抽象化が利用できるからです。スマートポインター、コンテナークラス、およびRAIIのようなデザインパターンにより、生のポインターを使用する必要性が減少します。ただし、抽象化と同様に、それらが実際にどのように機能するかを理解してから、それらを超えて移動する必要があります。


11

比較的簡単に言えば、Cの考え方は「問題がありますか?ポインターを使用してください」です。これは、C ++の初期のメンバーポインターを使用した場合でも、C文字列、関数ポインター、反復子としてのポインター、ポインターからポインター、ボイドポインターで見ることができます。

ただし、C ++では、これらのタスクの多くまたはすべてに値を使用できます。関数の抽象化が必要ですか?std::function。関数である値です。std::string?値であり、文字列です。C ++全体で同様のアプローチを見ることができます。これにより、人間とコンパイラの両方にとってコードの分析が非常に簡単になります。


C ++の場合:問題が発生しましたか?ポインターを使用します。今、あなたは... 2つの問題を抱えている
ダニエルZazula

10

理由の1つは、ポインターの適用範囲が広すぎることです。これらは、コンテナの反復に使用でき、関数に渡す際の大きなオブジェクトのコピー、非自明なライフタイム管理、メモリ内のランダムな場所へのアクセスなどを回避するために使用できます。意図的にすぐに独立。

正確な目的のためにツールを選択すると、コードがよりシンプルになり、意図がより明確になります-反復用のイテレーター、ライフタイム管理用のスマートポインターなど


3

すでに挙げた理由に加えて、明らかな理由があります:より良い最適化。エイリアス解析はポインター演算の意味で非常に複雑ですが、参照は最適化のヒントとなるため、参照のみを使用すると、より詳細なエイリアス解析が可能になります。


2

@jmquigleyポインターとポインター演算によって示されるメモリリークのリスクに加えて、ポインターはメモリ内のあらゆる場所を指すことがあるため、「バグを見つけるのが困難」および「セキュリティの脆弱性」を引き起こす可能性があります。

それが、C#とJavaでほぼ放棄された理由です。


C#で「アベンド」されていないことを期待してください。この答えは貧弱で、綴りが恐ろしく、不正確な記述があります。
ラムハウンド

1
彼らはほとんど放棄されました。ネイティブスピーカーになっていないことをおizeびします。
k3b

1
ねえ、私たちはスペルを手伝うことができます。あなたは期待する、または除外するという意味ですか?
DeveloperDon

1
C#ではほぼ廃止されましたが、unsafeキーワードを指定することでポインターを有効にすることができます
-linquize

-1

C ++は、ほとんどのC機能、およびオブジェクトとクラスをサポートします。Cには既にポインターやその他のものがありました。

ポインターは非常に便利な手法であり、オブジェクト指向と組み合わせることができ、C ++はそれらをサポートします。しかし、この手法は教えることも理解することも難しく、望ましくないエラーを引き起こすことは非常に簡単です。

多くの新しいプログラミング言語は、Java、.NET、Delphi、Vala、PHP、Scalaなどのオブジェクトでポインターを使用しないふりをしています。しかし、「舞台裏」でポインターが使用されます。これらの「隠されたポインター」技術は「参照」と呼ばれます。

とにかく、オブジェクト指向プログラミングと同様に、特定の問題を解決する有効な方法として、ポインターをプログラミングパターンと見なします

他の開発者は異なる意見を持っているかもしれません。しかし、学生とプログラマーに次の方法を学ぶことをお勧めします。

(1)オブジェクトなしでポインターを使用する

(2)ポインターのないオブジェクト

(3)オブジェクトへの明示的なポインター

(4)オブジェクトへの「隠された」ポインタ(別名参照);-)

その順序で。

教えるのが難しくて、学ぶのが難しいとしても。Object Pascal(Delphi、FreePascal、その他)およびC++(JavaまたはC#ではありません)は、これらの目標に使用できます。

そして、後に、初心者プログラマーは、Java、C#、オブジェクト指向PHPなどのような「オブジェクトへの隠されたポインター」プログラミング言語に移行できます。


19
C ++は、当初の「C with Classes」よりもはるかに優れています。
デビッドソーンリー

なぜC ++とCを空中引用符で囲むのですか?そして、「隠された」、「参照」、その他すべて?あなたは「セールスマン」であり、「プログラミング」に参加していませんか?
フレネル14年

それらを太字にする必要があります。引用符は、ハイライトにも反対するために使用することができます
umlcat

-6

VC6について言えば、インスタンス化するクラスのポインターを変数(DWORDなど)にキャストすると、このポインターがローカルであっても、同じヒープを使用するすべての関数でクラスにアクセスできます。インスタンス化されたクラスはローカルとして定義されますが、実際にはそうではありません。私の知る限り、ヒープ変数、構造体、またはクラスのアドレスは、ホスティングクラスのすべての期間にわたって一意です。

例:

class MyClass1 {
    public:
        void A (void);
        void B (void);
        void C (void);
    private:
        DWORD dwclass;
};

class MyClass2 {
    public:
        int C (int i);
};

void MyClass1::A (void) {
    MyClass2 *myclass= new MyClass2;
    dwclass=(DWORD)myclass;
}

void MyClass1::B (void) {
    MyClass2 *myclass= (MyClass2 *)dwclass;
    int i = myclass->C(0); // or int i=((MyClass2 *)dwclass)->C(0);
}

void MyClass1::B (void) {
    MyClass2 *myclass= (MyClass2 *)dwclass;
    delete myclass;
}

編集元のコードのごく一部です。CSRecodsetクラスは、すべての実際のコードがあるCXdbRecordsetのキャストクラスのみです。そうすることで、ユーザーが自分の書いたものを自分の権利を失うことなく利用できるようにすることができます。データベースエンジンがプロフェッショナルであることを示すふりはしませんが、実際に機能します。

//-------------------------------------
class CSRecordSet : public CSObject
//-------------------------------------
{
public:
    CSRecordSet();
    virtual ~CSRecordSet();
    // Constructor
    bool Create(CSDataBase* pDataBase,CSQueryDef* pQueryDef);
    //Open, find, close
    int OpenRst(bool bReadBlanks=0,bool bCheckLastSql=0,bool bForceLoad=0,bool bMessage=1);     // for a given SQL
    int FindRecord(bool bNext);         // for a given SQL
    // TABLE must be ordered by the same fields that will be seek
    bool SeekRecord(int nFieldIndex, char *key, int length=0);  // CRT bsearch
    bool SeekRecord(int nFieldIndex, long key);     
    bool SeekRecord(int nFieldIndex, double key, int decimals);     
    bool SeekRecord(XSEK *SEK);     
    bool DeleteRecord(void);
    bool Close(void);
    // Record Position:
    bool IsEOF(void);           // pointer out of bound
    bool IsLAST(void);          // TRUE if last record
    bool IsBOF(void);           // pointer out of bound
    bool IsOpen(void);
    bool Move(long lRows);      // returns FALSE if out of bound
    void MoveNextNotEof(void);  // eof is tested
    void MoveNext(void);        // eof is not tested
    void MovePrev(void);        // bof is tested
    void MoveLast(void);
    void MoveFirst(void);
    void SetAbsolutePosition(long lRows);
    long GetAbsolutePosition(void);
    void GoToLast(void); // Restore position after a Filter
    // Table info
    long GetRecordCount(void);
    int GetRstTableNumber(void);
    int GetRecordLength(void); //includes stamp (sizeof char)
    int GetTableType(void);
    // Field info
    int GetFieldCount(void);
    void GetFieldName(int nFieldIndex, char *pbuffer);
    int GetFieldIndex(const char *sFieldName);
    int GetFieldSize(int nFieldIndex);
    int GetFieldDGSize(int nFieldIndex); // String size (i.e. dg_Boolean)
    long GetRecordID(void);
    int GetStandardFieldCount(void);
    bool IsMemoFileTable(void);
    bool IsNumberField(int nFieldIndex);
    int GetFieldType(int nFieldIndex);
    // Read Field value
    bool GetFieldValue(int nFieldIndex, XdbVar& var);
    bool GetFieldValueIntoBuffer(int nFieldIndex,char *pbuffer);
    char *GetMemoField(int nMemoFieldIndex, char *pbuffer, int buf_size);
    bool GetBinaryField(unsigned char *buffer,long *buf_size);
    // Write Field value
    void Edit(void); // required
    bool SetFieldValue(int nFieldIndex, XdbVar& var);
    bool SetFieldValueFromBuffer(int nFieldIndex,const char *pbuffer);
    bool Update(void); // required
    // pointer to the same lpSql
    LPXSQL GetSQL(void);
};

//---------------------------------------------------
CSRecordSet::CSRecordSet(){
//---------------------------------------------------
    pClass |= (CS_bAttach);
}
CSRecordSet::~CSRecordSet(){
    if(pObject) delete (CXdbRecordset*)pObject;
}
bool CSRecordSet::Create(CSDataBase* pDataBase,CSQueryDef* pQueryDef){
    CXdbQueryDef *qr=(CXdbQueryDef*)pQueryDef->GetObject();
    CXdbTables *db=(CXdbTables*)pDataBase->GetObject();
    CXdbRecordset *rst = new CXdbRecordset(db,qr);
    if(rst==NULL) return 0;
    pObject = (unsigned long) rst;
    return 1;
}
bool CSRecordSet::Close(void){
    return ((CXdbRecordset*)pObject)->Close();
}
int CSRecordSet::OpenRst(bool bReadBlanks,bool bCheckLastSql,bool bForceLoad, bool bMessage){
    unsigned long dw=0L;
    if(bReadBlanks) dw|=SQL_bReadBlanks;
    if(bCheckLastSql) dw|=SQL_bCheckLastSql;
    if(bMessage) dw|=SQL_bRstMessage;
    if(bForceLoad) dw|=SQL_bForceLoad;

    return ((CXdbRecordset*)pObject)->OpenEx(dw);
}
int CSRecordSet::FindRecord(bool bNext){
    return ((CXdbRecordset*)pObject)->FindRecordEx(bNext);
}
bool CSRecordSet::DeleteRecord(void){
    return ((CXdbRecordset*)pObject)->DeleteEx();
}
bool CSRecordSet::IsEOF(void){
    return ((CXdbRecordset*)pObject)->IsEOF();
}
bool CSRecordSet::IsLAST(void){
    return ((CXdbRecordset*)pObject)->IsLAST();
}
bool CSRecordSet::IsBOF(void){
    return ((CXdbRecordset*)pObject)->IsBOF();
}
bool CSRecordSet::IsOpen(void){
    return ((CXdbRecordset*)pObject)->IsOpen();
}
bool CSRecordSet::Move(long lRows){
    return ((CXdbRecordset*)pObject)->MoveEx(lRows);
}
void CSRecordSet::MoveNextNotEof(void){
    ((CXdbRecordset*)pObject)->MoveNextNotEof();
}
void CSRecordSet::MoveNext(void){
    ((CXdbRecordset*)pObject)->MoveNext();
}
void CSRecordSet::MovePrev(void){
    ((CXdbRecordset*)pObject)->MovePrev();
}
void CSRecordSet::MoveLast(void){
    ((CXdbRecordset*)pObject)->MoveLast();
}
void CSRecordSet::MoveFirst(void){
    ((CXdbRecordset*)pObject)->MoveFirst();
}
void CSRecordSet::SetAbsolutePosition(long lRows){
    ((CXdbRecordset*)pObject)->SetAbsolutePosition(lRows);
}
long CSRecordSet::GetAbsolutePosition(void){
    return ((CXdbRecordset*)pObject)->m_AbsolutePosition;
}
long CSRecordSet::GetRecordCount(void){
    return ((CXdbRecordset*)pObject)->GetRecordCount();
}
int CSRecordSet::GetFieldCount(void){
    return ((CXdbRecordset*)pObject)->GetFieldCount();
}
int CSRecordSet::GetRstTableNumber(void){
    return ((CXdbRecordset*)pObject)->GetRstTableNumber();
}
void CSRecordSet::GetFieldName(int nFieldIndex, char *pbuffer){
    ((CXdbRecordset*)pObject)->GetFieldName(nFieldIndex,pbuffer);
}
int CSRecordSet::GetFieldIndex(const char *sFieldName){
    return ((CXdbRecordset*)pObject)->GetFieldIndex(sFieldName);
}
bool CSRecordSet::IsMemoFileTable(void){
    return ((CXdbRecordset*)pObject)->IsMemoFileTable();
}
bool CSRecordSet::IsNumberField(int nFieldIndex){
    return ((CXdbRecordset*)pObject)->IsNumberField(nFieldIndex);
}
bool CSRecordSet::GetFieldValueIntoBuffer(int nFieldIndex,char *pbuffer){
    return ((CXdbRecordset*)pObject)->GetFieldValueIntoBuffer(nFieldIndex,pbuffer);
}
void CSRecordSet::Edit(void){
    ((CXdbRecordset*)pObject)->Edit();
}
bool CSRecordSet::Update(void){
    return ((CXdbRecordset*)pObject)->Update();
}
bool CSRecordSet::SetFieldValue(int nFieldIndex, XdbVar& var){
    return ((CXdbRecordset*)pObject)->SetFieldValue(nFieldIndex,var);
}
bool CSRecordSet::SetFieldValueFromBuffer(int nFieldIndex,const char *pbuffer){
    return ((CXdbRecordset*)pObject)->SetFieldValueFromBuffer(nFieldIndex,pbuffer);
}
bool CSRecordSet::GetFieldValue(int nFieldIndex, XdbVar& var){
    return ((CXdbRecordset*)pObject)->GetFieldValue(nFieldIndex,var);
}
bool CSRecordSet::SeekRecord(XSEK *SEK){
    return ((CXdbRecordset*)pObject)->TableSeek(SEK);
}
bool CSRecordSet::SeekRecord(int nFieldIndex,char *key, int length){
    return ((CXdbRecordset*)pObject)->TableSeek(nFieldIndex,key,length);
}
bool CSRecordSet::SeekRecord(int nFieldIndex,long i){
    return ((CXdbRecordset*)pObject)->TableSeek(nFieldIndex,i);
}
bool CSRecordSet::SeekRecord(int nFieldIndex, double d, int decimals)
{
    return ((CXdbRecordset*)pObject)->TableSeek(nFieldIndex,d,decimals);
}
int CSRecordSet::GetRecordLength(void){
    return ((CXdbRecordset*)pObject)->GetRecordLength();
}
char *CSRecordSet::GetMemoField(int nMemoFieldIndex,char *pbuffer, int BUFFER_SIZE){
    return ((CXdbRecordset*)pObject)->GetMemoField(nMemoFieldIndex,pbuffer,BUFFER_SIZE);
}
bool CSRecordSet::GetBinaryField(unsigned char *buffer,long *buf_size){
    return ((CXdbRecordset*)pObject)->GetBinaryField(buffer,buf_size);
}
LPXSQL CSRecordSet::GetSQL(void){
    return ((CXdbRecordset*)pObject)->GetSQL();
}
void CSRecordSet::GoToLast(void){
    ((CXdbRecordset*)pObject)->GoToLast();
}
long CSRecordSet::GetRecordID(void){
    return ((CXdbRecordset*)pObject)->GetRecordID();
}
int CSRecordSet::GetStandardFieldCount(void){
    return ((CXdbRecordset*)pObject)->GetStandardFieldCount();
}
int CSRecordSet::GetTableType(void){
    return ((CXdbRecordset*)pObject)->GetTableType();
}
int CSRecordSet::GetFieldType(int nFieldIndex){
    return ((CXdbRecordset*)pObject)->GetFieldType(nFieldIndex);
}
int CSRecordSet::GetFieldDGSize(int nFieldIndex){
    return ((CXdbRecordset*)pObject)->GetFieldDGSize(nFieldIndex);
}
int CSRecordSet::GetFieldSize(int nFieldIndex){
    return ((CXdbRecordset*)pObject)->GetFieldSize(nFieldIndex);
}

編集:DeadMGからのリクエスト:

void nimportequoidumomentquecaroule(void) {

    short i = -4;
    unsigned short j=(unsigned short)i;

}

1
この説明は、説明する内容を示すために、いくつかのコードによって大幅に強化される可能性があります。元の質問に関連していると感じていますが、このシナリオで危険を警告された場合、質問者のトピックを詳しく説明するのに役立ちます。
DeveloperDon

1
キャストDWORDは不正であり、おそらく不正です(DWORDは必ずしもポインターを保持するのに十分な幅ではありません)。型指定されていないポインタが必要な場合は、void*-を使用しますが、C ++でそれが必要な場合は、修正する必要のあるコードに設計上の問題があることがよくあります。
マット

サルバドール、あなたはVC6について、そしてそれが異常で予想外のポインター処理をどのように持っているかについて何かを言おうとしていると思います。この例にはコメントが役立つ可能性があります。コンパイラーから警告が表示されている場合は、質問への回答の関連付けに関して有益である可能性があります。
DeveloperDon

@Matこの例は、32ビットOSおよびVC6 ++コンパイラー用です。
サルバドール

6
絶対に古代のコンパイラの悪いコードについて話していますか?結構です。
DeadMG
弊社のサイトを使用することにより、あなたは弊社のクッキーポリシーおよびプライバシーポリシーを読み、理解したものとみなされます。
Licensed under cc by-sa 3.0 with attribution required.