2
5

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

More than 1 year has passed since last update.

C#使いがC++を学習してみて戸惑ったこと(学習用メモ)(随時更新予定)

Last updated at Posted at 2022-01-09

あらまし

VST開発がしたくて、仕事でC♯を使っている筆者はC++を学習することにした(AudioPlugSharpという便利なツールもあるが、開発者としての腕試しをしたいのと、やれることの幅を広げたいというのもあって、VST開発の基本言語であるC++を使うことにした)。
なお、開発環境はWindows11, Visual Studio 2022である。

C言語に触っていたのは大学時代だから、今から10年近く前だ。
ポインタなどは割と躓きポイントとして知られるが、当時四苦八苦した経験があり、何とか理解できそうである。
しかし、C#の利便性に慣れっこだった筆者が戸惑ったのは、そこではなかった。

将来的にはぼやきを整理して、C#コーダー向けのC++入門にしたいなあと思う。

参考文献

おそらく今日本語でC++を学ぶなら、この本をケチらず買うのがベストだと思われる。

関数やクラスは読みだされる順に書かないといけない!

例えば、以下のようなプログラムは動かない。

thisCanNotBeBuilt.cpp
#include <iostream>
#include <string>
using namespace std;

class Piyo {
    Hoge hoge;
public:
    Hoge getHoge() {
        return hoge;
    }
    void setHoge(Hoge value) {
        hoge = value;
    }
};

struct Hoge {
    void doSomthing() {
        cout << "This class is " << toString() << endl;
    }
    string toString() {
        return "Hoge";
    }
    void setId(int value) {
        id = value;
    }
    int getId() {
        return id;
    }
private:
    int id;
};

int main()
{
    Piyo piyo;
    Hoge hoge;
    hoge.setId(7);
    piyo.setHoge(hoge);

    cout << piyo.getHoge().toString() << endl;
};

何故かというと、PiyoHogeより前に定義されているから。ご丁寧なことに、ビルドしてみないと間違えていることを教えてくれない
どれだけJava以降の言語が有情かということを痛感した。
Javaとかが気の回って主人の思い至らない点を取りなしてくれる木下藤吉郎なら、C++は頼まれたことを確実にやるが、それ以外のことは全くやらない杓子定規な堅物(そんなタイプ偉人にならないだろうなあ)な感じである。

対策

PiyoHogeの順番を入れ替える。
組み込み型の関数の場合、プロトタイプを宣言すれば順番が前後しても大丈夫になるが、少なくともVSの場合はclassおよびstructで同様のことはできない。

例えば、以下の関数は動くが、

programWithPrototype.cpp
#include <iostream>
using namespace std;

void display();

int main()
{
    display();
};

void display() {
    cout << "何か表示" << endl;
}

以下のプログラムは駄目である。

anotherProgramThatFailsToBeBuilt.cpp
#include <iostream>
#include <string>
using namespace std;

struct Hoge; // 未定義のクラスが使われていますというエラーが出る。

class Piyo {
    Hoge hoge;
public:
    Hoge getHoge() {
        return hoge;
    }
    void setHoge(Hoge value) {
        hoge = value;
    }
};

struct Hoge {
    void doSomthing() {
        cout << "This class is " << toString() << endl;
    }
    string toString() {
        return "Hoge";
    }
    void setId(int value) {
        id = value;
    }
    int getId() {
        return id;
    }
private:
    int id;
};

int main()
{
    Piyo piyo;
    Hoge hoge;
    hoge.setId(7);
    piyo.setHoge(hoge);

    cout << piyo.getHoge().toString() << endl;
};

C++ではソースの順番に気をつけなきゃね、という話でした。

(2022/01/20追記)クラス定義が違いすぎる

C#のクラス定義では、メソッドを内包するのが当たり前。

Class.cs
public class Foo
{
    private double Bar()
    {
        return Math.Pi;
    }
    
    private int Baz()
    {
        return (int)Bar();
    }
}

しかし、C++はメソッドの実装を外部から注入するのがデフォルト。

class.cpp
class Foo {
    double bar() const;
public:
    int baz() const;
}

inline double Foo::bar() const {
    return 3.1415926536;
}
int Foo::baz() const {
    return (int)this->bar();
}

外部からは不可知のはずのprivateメソッドまで外で実装するのがまだ慣れない。これはC++がC同様メソッドなどの定義用にヘッダを使う言語だからだと考えられる(Cだってヘッダと実装が分かれていた)。正直視認性やメソッド定義の抜け漏れを考えるとC#やJavaのようにメソッド定義をクラス内部で行う形式の方が良いと思う。C++の書き方だとメソッド指定は倍以上になるしね(「(型名) (クラス名)::」とあとconstなどの後置指定子が漏れなくついてくるため。せめて戻り値の型の省略ができればなあ。戻り値の型だけが同じ関数を多重定義できないわけだし、納得行きません)。

実際にはクラス内部にメソッドを定義可能だが、そのメソッドはinline指定されたのと同じになり、コードを最適化する際にメソッドの処理が直接呼び出し先にぶっ込まれてしまう。そのため今回のような短いメソッドは内部で定義しても構わないが、長いメソッドは推奨されない(そもそもメソッドが長い時点でリファクタリングをかけるべきなのだが)。C#使いとしては逆にメソッドを必ず読みだして使うnoinline指定があれば嬉しかったのだが。

(2022/01/20追記)inline指定とconst指定

C#に搭載されていないメソッド指定として、inline指定とconst指定が挙げられる。

inlineは先ほど説明した通りなので詳細は省くが、ヘッダにしか記載できないことは追記しておく。C#のLinq to SQLなどでSelectなどの条件にカスタムメソッドを使えないので、冗長な条件記載を省くためにもinline指定があると大変助かる(C#の場合はMethodImplAttributeによる最適化の大まかなレベル指定しか出来ない)。

constmutableフィールド以外の内部の状態を変更しないメソッドに付けられる。つまり、メソッドの中で非mutableのフィールドを変更するとコンパイルエラーになるということ。この辺りはC++の徹底的な最適化を施す設計があると思われるが、意図しないインスタンスの変更は割とあるので、他の言語にも同様の機能が欲しいところだ。

(2022/01/25追記)コンストラクタの読み出し方

C#やJavaでは、インスタンス化は以下の様に書く。

.cs
Foo foo = new Foo();

しかしC++はシンプルさでそのさらに上をいく。

.cpp
Foo foo;

これでクラスをインスタンス化出来てしまうのだからすごい。ただ、newはC++にもあって、ポインタ式である。

.cpp
Foo *foo = new Foo;
// 処理
delete foo;

コードにも書いたとおり、明示的にdeleteを書かなければいけない超絶面倒かつ不具合の温床となっているまずい仕様であり、今はスコープから外れればリソースが解放される(RAII)前者の方式が主流である。ちなみにガベジコレクションなんて実装が面倒くさい機構は存在しない。
なお、コンストラクタに引数を渡したい場合は以下の様にすればOK。

.cpp
Foo foo(bar);
// コンストラクタにexplicit指定子が付いていない場合
Foo foo2 = bar;

後半の書き方は直感的でないので、基本explicitは付けたほうが良さげだろう。アップ(ダウン)キャストが必要な場合はdynamic_castを使うべき。

(2022/01/25追記)コンストラクタの定義

コンストラクタの書き方も違う。

.cpp
class Foo
{
    int _bar;
public:
    Foo();
    Foo(int bar);
// アクセサは省略
}
Foo::Foo() : _bar(0) { }
Foo::Foo(int bar) : _bar(bar) { }
// Foo::Foo() : Foo(0) { } と書いてもよい

コンストラクタのコロンの後にフィールドの初期値を指定する形式である。

対してC#で同様の処理を書こうとするとこうなる。

.cs
public class Foo
{
    private int _bar;
    public Foo() : Foo(0) { }
    public Foo(int bar)
    {
        _bar = bar;
    }
}

こちらの方が明らかに直感的だし、プロパティならアクセサ経由で設定したほうが不正な値をチェックできるようになるので安全でもある。
この辺りのショートサーキットぶりはC++の効率重視ぶりがうかがえる。

(2022/01/25追記) 継承時にアクセス指定子を指定

C#の場合、派生クラスには public に属する基底クラスのメソッドやフィールドをデフォルトで使うことができるが、C++では継承元クラスの前にアクセス指定子を書く必要がある。 protected にすると public メンバが派生クラスの protected 扱いになり、 private にすると public メンバや protected メンバが派生クラスの private メンバとなる。正直ここまでせんでも、と思わなくもない。後発言語がC++における public 継承をメインとした理由は何となくわかる。C++でこの仕様が考えられたのは多重継承で関数がバッティングするのを最小限に抑えるためだろうか。

(2022/01/30追記) 純粋仮想関数(C#でいうところのabstract

基底クラスで実装を定義したくない関数を作る際には以下のようにしなければならない。

.cpp
class WithPVF
{
public:
    virtual int PVF() = 0;
}

C++使いなら知ってて当然なイディオムかもしれない(=0がヌルポインター扱い?)が、正直分かり辛い。他の言語みたく特殊なキーワードをつけてほしかったところである。C#形式が「これは抽象クラスです。直接使用できません」というのが分かりやすい。

クラスを複数継承できることを逆手に取り、仮想関数のみのクラスを継承させてinterfaceの代わりにするハックも横行している。だったらトレイト的にデフォルト処理をvirtual実装しておきたい。

(2022/02/01追記) 悪名高いfriend

まさに過去の負の遺産と言っても過言では無い。カプセル化を壊してどうすんの、と言いたくなる。使う局面があるとしたら設計自体をやり直した方が良いのではないか。
ファクトリ関数を作るならクラスのstaticメソッドを使った方が明示的だろう。でも有効な使い方があるようだ…

(2022/11/04追記) 演算子オーバーロードが面白い

演算子オーバーロードは両方にある機能で、よく似ている部分も多いが、こんなことが違う。

機能 C# C++
実装方式 staticメソッドのみ インスタンスメソッド(左辺が当該クラス)
もしくは friend メソッド(辺の制限無し)
単項演算子 1引数 0引数(インスタンスメソッド)
2項演算子 2引数 1引数(インスタンスメソッド)
引数が右辺になる
++, -- 前置のみ定義、後置は自動実装 両方とも定義すべき
前置は0引数、
後置はint 型(のみ) を引数に取る
true, false オーバーロード可 オーバーロード不可
&, | 論理演算子(ショートカット無し版)
(C#のビット演算子は特定型1のみ定義)
ビット演算子
&&, || &, |, true, false
オーバーロードから
ショートカット版を自動生成
こちらを直接オーバーロードできない
論理演算子
添字演算子 インデクサーで定義 オーバーロードで定義
ポインタ、参照 オーバーロード可 オーバーロード不可
() (関数読み出し) オーバーロード不可 オーバーロード可
=(代入) オーバーロード不可 オーバーロード可
(コピー&ムーブ)
複合代入 左側の演算子から自動生成 いちいち定義
==, != 両方定義必要 片方のみでOK
(というか縛りはほとんど無し)
<, > 両方定義必要 片方のみでOK
<=, >= 両方定義必要 片方のみでOK
変換関数 そんなの無い opeartor 型()で可能
explicitで明示変換のみ受け付け

こうやって見るとC++の自由度の高さが伺える(だからと言って何でもやっていい訳じゃ無いよ!)し、制限がきつめのC#の変換も理に適っているものが多い。ただ、!===falseにすればいいはずなので未定義なら自動生成してもいいんじゃないかと思わなくもない。

C++で嬉しいのは変換関数。C#ユーザにも需要高いのではないだろうか(基本型から継承型に変換できたらなあというときは大石内蔵助。ただ派生型の情報を知っている時点でヤバい気がする…)。

そして、演算子オーバーロードがおそらく唯一の friend メソッド利用が正当化される理由である(Javaが演算子をオーバーロードできない理由はこれだろうな)。C++とC#の設計思想の違いがあぶりだされるいい比較だと思う。

(2022/11/05追記) 関数ポインタ型とラムダ式

C#は関数にFunc<戻り値の型>というジェネリッククラスをあてがうことで、直感的に分かりやすいものになっている。対してC++では、C言語の伝統を受け継ぎこんな形になる。

戻り値の型 (*変数名)(引数の型リスト)

慣れないと何が変数名なのかわからない。この辺りもC#が後発言語として二の轍を踏まない設計がされていることがわかる。

ラムダ式もC++はよくあるやつと違う。C#やJavaScriptのアロー演算子では(引数リスト) => {中身}という書き方であるが、C++は[キャプチャしたい変数](引数リスト) -> 戻り値の型 mutableなど指定{中身} となる。ここで-> 戻り値の型 は戻り値で推測する場合は省略できる。その場合 mutable などの指定は (引数リスト) の直後に書くことになる。

矢印が => でないのは他の言語と違い1行即リターン(例: (i) => i + 1; で引数に1足した値が返ってくる)を採用できないからだろう(mutableなどの指定をどうするのという話になるから)。

変数キャプチャはC#ではデフォルトでONだ。つまり引数名が外のスコープと被るのは許されない。この辺り細かく指定できるのもC++のチューンナップ性の高さ(逆に言えば環境側で余計なことをしない)が伺える。

つまり、C++のラムダ式は他の言語の仕様を安易に取り入れるのではなく、C++の特性をしっかり踏まえた文法として完成度の高い整備がなされていると言えよう。

(2022/11/05追記) private virtual なメソッド

C#の常識として、 private かつ virtual なメソッドはあり得ない。「仮想または抽象メンバーには、privateを指定できません」と怒られる。しかしC++は違い、こんなコードが通る。

VirtualPrivate.hpp
#pragma once
class VirtualPrivate
{
private:
    virtual void secret() = 0;
public:
    void doSomething() { seclet(); }
};
Inherited.hpp
#pragma once
#include "VirtualPrivate.hpp"
class Inherited : public VirtualPrivate
{
private:
    void secret() override;
};
Inherited.cpp
#include "Inherited.hpp"
#include <iostream>

void Inherited::secret()
{
    std::cout << "秘密のメソッド!" << std::endl;
}
main.cpp
#include "Inherited.h"

int main()
{
    Inherited inherited;
    inherited.doSomething(); // 基底クラスのdoSomething経由でこのクラスのsecretが実行される
}

このおかげで protected 指定子の意味があまり無いとも言える(メソッドの共通化ならクラスを切り出した方が応用が利く)が、親クラスが子クラスの private メソッドを呼び出すというのはどうも気持ちが悪い。仮想メソッドは protected とした方が「継承させる」というのが伝わりやすいのではなかろうか。

  1. int, uint, long, ulong 型のみ定義。shortなどintに変換できる型は変換して実行される。

2
5
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
2
5

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?