TP-Link AC600のLinux向けデバイスドライバ
自宅のマシンをUbuntu 22.04にアップデートしたのだけど、毎回WiFiアダプタのデバイスドライバが標準搭載されていないのを忘れて右往左往している。そして毎回、あちこちから拾ってきたデバイスドライバのコンパイルバトルになるので、今回やったメモを残しておく(やっと、メモを残し始めたところ)。
TP Link AC600の中身
lsusb すると下記のような情報が出てくる。つまりRealtekのRTL8811AUというPHYが載っているということだ。
ID 2357:011e TP-Link AC600 wireless Realtek RTL8811AU [Archer T2U Nano]
デバイスドライバその1(今回使ったもの)
今回、色々探してみたあげく、良さそうかなと思ったドライバの一つ。
READMEには色々ゴチャゴチャ書いてあるが、結局の所、MakefileでRTL8821Aのコンフィギュレーションを設定してDKMSでインストールするだけだ。
########################## WIFI IC ############################ CONFIG_RTL8812A = n CONFIG_RTL8821A = y CONFIG_RTL8814A = n
sudo make dkms_install
ただ、このままだと、DKMSのインストールの処理でコケる。koファイルのファイル名を決め打ちにしているところがあるのかな。同じく、Makefile内で下記のように修正する。
MODULE_NAME := 8811au
↓
MODULE_NAME := 88XXau
これをやると上記のDKMSのインストールの処理も通るようになる。DKMSに詳しくないのでこれ以上掘り下げられない。ごめん。
ただ、曲がりなりにも8811auのドライバが使えたことに感謝。
なお、今回試したのはブランチ v5.6.4.2 の最新コミット 37e27f916。
デバイスドライバその2(今回使わなかったもの)
もう一つ、こういうのもあった。お題目的には前記のドライバと同じだ。
しかし、前記のドライバ同様に石の設定を8821aにしてみたところではビルドが通らなかった。対象のヘッダファイルがないとかなんとかで完全にビルドが通らない状況なので、もう解析するのはやめた。。。
まぁ、要するに8821aをサポートする気がないんだろう。(そもそも8812auのドライバだしね)
ということで
適切かどうかの判断もできていないが、標記デバイスのデバイスドライバをDKMS対応で入れることができた。
デバイスドライバのコンフィギュレーションって、変えるとすぐにビルド通らなくなったりして、特定の構成でしか評価していないんだなとはっきりわかる。それでもこうやって使えているのだから感謝しかないけど。
「おっさんのためのModern C++入門ガイド」をZenn Bookにしてみた
もしかするとWeb記事のままだと読みにくいかなとか思い、Zenn Bookにしてみた。
少しだけ内容を見直した(あまりに足りないかなというところだけ)がだいたいそのままの内容。
ひどいところがあれば直していこうと思うのでよろしければご指摘ください。
おっさんのためのModernC++入門ガイド(草稿)
みなさんはC++の読み書きができますか?
自信がある方、いつ頃勉強しましたか?もし20世紀に勉強したのであれば、その知識は相当古いです。実質現在のModernC++(C++11以降のC++)とは概念上の互換性がないので脳のアップデートが必要です。
自信がない方、文法は知っているけどなんとなく使いこなせていない方、マサカリ屋にあーだこーだ言われて大混乱している方。必勝パターンを身につければもっと楽にコードを読み書きできるようになるかもしれません。
この文章の目的は、ModernC++におけるメンタルモデル(考え方)や必勝パターンをざっくりと導入することでみなさんが楽にModernC++を読み書きできるようなお手伝いをすることです。主要な内容としてはムーブセマンティクスと右辺値 とその次の章でだいたいA4換算で15ページくらい?ほかは正直流し読みしてもらえるような内容です。また、内容的にはそのボリューム以上に薄いので気楽に読んでもらって、気軽にコードが書けるようになってもらえればいいなと思います。
追記:この記事の本稿はZenn Bookにして公開しています。誤植修正等は下記のリンク先のZenn Bookで実施します。よろしければこちらをどうぞ。
前置き
ModernC++宣言
ModernC++を書く前に言っておきたい事がある かなり厳しい話もするが俺の本音を聴いておけ new/deleteは(直接)呼んではいけない 生ポインタも使ってはいけない auto推論を使え、範囲forを使え できる範囲で構わないから 忘れてくれるな 所有権を理解しない者に まともなコードは書けないことを お前にはお前しか できない事もあるから 細かいことはライブラリに任せて ロジックの実現に集中しろ
意訳:
ModernC++ではこれまで多用されてきた new
/ delete
などのハンドコードによるメモリ管理が基本的に排除され、スコープとスマートポインタの概念で基本的なアロケーションの管理ができるようになっています。また、アロケーションの管理だけでなく、よりシンプルに記述できる構文規則を採用することで、実現したいロジックをより簡潔に表現できるようになっています。
ただし、この恩恵を受けるためには「明快な意味論」であるところのムーブセマンティクスをある程度理解している必要があります。
何をするか?
このガイドは主にこれまで、なんとなくC++ の構文を知ってたり、レガシーなC++なら読み書きできる人(ここでは単に短く「おっさん」と呼ぶ)が、モダンで実用的なC++アプリケーションコードをかけるようにするためのModernC++クイックスタートガイドを提供します。
ライブラリ利用者の立ち位置から簡潔な記述でアプリケーションコードを書けるレベルを目指します。基本的に、大胆不敵に説明し、大枠を掴んでリテラシーを確立することを目的にします。細かいところが間違っていても、後で調べてわかりゃそれでいいじゃないですか。
以降、本ガイドの内容についてもう少し説明します。(かったるそうだなと思ったらセクションの終わりまで読み飛ばしてもらっても大丈夫です。)
大まかには下記のような流れになります。
-
主にムーブセマンティクスの直感的理解と変数/関数の宣言における必勝パターンの把握を目指します。
-
このガイド内での説明のほとんどをこれに割きます。
-
これをある程度理解ししていれば、あとは些末だと言っても過言ではありません。
-
-
高速化その他の特殊な事情でデフォルトのメモリアロケーション機構を置き換えるなどの発展的な利用法については触れません。
-
自分でテンプレートを書くレベルまで到達することは想定しません。
-
つまり次のような言葉は過去に耳にしていても一旦忘れろってことです。これらの概念はテンプレート書かない段階では基本的に関係ないです。
-
完全転送
-
ユニバーサル参照
-
参照圧縮
-
-
-
ライブラリ利用者の立ち位置でアプリケーションコードを書くという前提から、ある程度理解しておくべき事項を補足します。
対象のおっさんであることの確認
だいたい、次のことがある程度自分で説明できるか、事前に確認しておきましょう。
ちなみに、上記の内容に不安がある場合、このガイドを読むより、C++の一般的な教本を読んだほうが効率がいいかもしれません。
ムーブセマンティクスと右辺値
前提:C++のオブジェクトの生存期間
ムーブセマンティクスを語る前に、前提知識となる、C++のオブジェクトの生存期間について確認します。
C++においてオブジェクトを確保したとき、そのライフサイクルは構文の側面から見てざっくり2種類あります。
左辺値と右辺値です。
左辺値(lvalue)
名前のついたオブジェクトです。名前のスコープが切れたところでデストラクタが呼ばれます。
#include <vector>
void func(){
std::vector<int> v;
// なにか処理1
// なにか処理2
// スコープが切れるところでvectorのデストラクタが呼ばれる
}
右辺値(rvalue)
右辺値は「はかない」オブジェクトです。名前で束縛しない限り、その式の中で消え去ってしまいます。
#include <vector>
void func(){
int i = (std::vector<int>{1,2,3}).pop();
// pop()の処理が終わったときにvectorのデストラクタが呼ばれる
// なにか処理1
// なにか処理2
}
この「はかない」右辺値がムーブセマンティクスと密接に関わってきます。
コピーとムーブ
ムーブセマンティクスのコアとなる、コピーとムーブの概念についてざっくり説明します。
次のような大きなデータを含む構造があったとします。
std::vector<int> someBigVector{0,1,2,..<<中略>>.., 1999999};
std::vector<int> otherBigVector0 = someBigVector;
std::vector<int> otherBigVector1(someBigVector);
一方で、 vector
の内部のデータを複製せず、ポインタの挿げ替えだけをした方が効率が良い場合が多々あります。たとえば、データ構造の構築を行うファクトリ関数とデータの受け渡しをする場合などです。この場合、データを作成した関数の側ではもうそのオブジェクトを使用しないので、中身をそのまま「移動」してあげたほうが効率がいいということになります。
過去のC++では、こういうときに次のような方法がありました。
ムーブコンストラクタ、ムーブ代入演算子の例
class BigArray{
private:
std::unique_ptr<char[]> big_data;
public:
BigArray(BigArray&& old) // ムーブコンストラクタ
:big_data(old.big_data)
{
old.big_data = nullptr;
}
operator=(BigArray&& old) // ムーブ代入演算子
{
this.big_data = old.big_data;
old.big_data = nullptr;
}
};
ムーブコンストラクタ、ムーブ代入演算子は新たにヒーブ上のデータを確保するわけではなく、既存のデータをすげ替えることだけをします。移動のもととなった old
オブジェクトは実質的に機能しなくなります。(以降、本ガイドではこれを「old
が破壊された」と表現します。)
ここで、いわゆるコピー系の機能(コピーコンストラクタ、代入演算子)とムーブ系の機能(ムーブコンストラクタ、ムーブ代入演算子)のシグニチャを比較してみましょう。とても頻繁に利用する std::vector
のシグニチャの一部を簡略化したものを挙げます。
std::vector
のシグニチャの一部namespace std {
template <typename T>
class vector
{
public:
vector(); // デフォルトコンストラクタ
vector(const vector&); // コピーコンストラクタ
vector(vector&&); // ムーブコンストラクタ
operator=(const vector&); // (コピー)代入演算子
operator=(vector&&); // ムーブ代入演算子
};
}
std::vector
クラスのオブジェクトはコピー系、ムーブ系、両方の演算を持っています。
コピー系とムーブ系の区別として、コンストラクタ/代入演算子ともに、コピー系の処理は引数の型が const vector&
になっているのに対し、ムーブ系の処理は引数の型が vector&&
になっています。「&&
ってなんだ?」となるかも知れませんが、これについて次のセクションでざっくり説明します。
# 便宜上、Allocator
に関するテンプレート引数を省略しました
ムーブコンストラクタ、ムーブ代入演算子の呼び出し方
謎の &&
という記号は右辺値参照と呼ばれるものです。関数の引数に書いた場合、右辺値にのみマッチします。なぜそのようなマッチングを取るのかというと、「破壊しても良い」対象を識別するためです。少なくとも、その式が完了したあとは破壊されるようなオブジェクトは「移動して破壊しても良い」と解釈されるということです。
std::vector v;
v = std::vector{0,1,2,3}; // std::vector{0,1,2,3} は右辺値
// 呼び出される演算子はムーブ演算子 std::vector::operator=(std::vector&&)
// 右辺で生成されたvectorはムーブされ破壊される
左辺値を引数にした場合、右辺値参照にはマッチしません。したがって、コピー代入が行われることになります。
std::vector v;
std::vector v1{0,1,2,3};
v = v1; // v1は名前をつけたので左辺値
// 呼び出される演算子はコピー代入演算子 std::vector::operator=(const std::vector&)
// 右辺にあるv1は左辺値なのでムーブされない
値返しする関数の戻り値も右辺値と認識されるので、ムーブ対象です。
std::vector some_function();
std::vector v;
v = some_function(); // std::vector{0,1,2,3} は右辺値
// 呼び出される演算子はムーブ演算子 std::vector::operator=(std::vector&&)
// 右辺で生成されたvectorはムーブされ破壊される
では、一度名前をつけてしまったオブジェクトは左辺値になるのでムーブできないのか、と言われるとそういうわけではありません。std::move
を使うことで、強制的に右辺値の扱いにすることができ、ムーブさせることができます。
ムーブしてしまったあとも左辺値のオブジェクトは残りますが、データとしては通常破壊されています。破壊されていることを型システム上検知する方法はないので、「気をつけて一度だけ実施してください」ということになります。
std::vector v0 = some_function(); // 名前をつけたので左辺値になった
std::vector v;
v = std::move(v0); // 左辺値だがstd::moveでキャストすることで強制的にムーブできる
// ただし、実行がここまで来るとv0の中身はムーブ済みになっていて空っぽである
右辺値参照によるマッチング
関数呼び出し時に右辺値参照でマッチングされたオブジェクトは「破壊して良いオブジェクト」と認識するのがC++の不文律です。破壊するつもりの関数引数には基本的に &&
をつけて宣言し、std::move
を使用してムーブ系の演算を実際に呼び出す関数まで渡すのがよくある記述パターンです。
MovableObj mc;
void moving_function1(MovableObj&& obj)
{
mc = std::move(obj); // ムーブ代入を呼び出す
}
std::vector<MovableObj> mv;
void moving_function2(MovableObj&& obj)
{
mv.push_back(std::move(obj)); // std::vector<T>::push_back(T&&)は右辺値参照を受け取ると内部でムーブする
}
ここで例に挙げた関数は左辺値を引数に与えようとしてもマッチしないことに気をつける必要があります。
関数引数の型マッチングの整理
右辺値参照を引数に持つ関数について説明してきましたが、ModernC++で一般に使われる関数引数の型のパターンについて整理します。
型のパターン | 典型的な使用ケース |
---|---|
|
データをコピーして使用する際に使用 |
|
元のデータを読み出す際に使用 |
|
元のデータを書き換える際に使用 |
|
元のデータをムーブして破壊する際に使用 |
T&&
に const
がつくことはその性質上、ありえないと言えます。
T&&
と const T&
/ T&
の間のオーバーロード関係について、T&&
のオーバーロードがない場合、右辺値に対して const T&
があればマッチします。T&
のオーバーロードがないときに const T&
にマッチするのと同じノリだと思ってもらって良いと思います。
気をつけたいのは T&&
と T&
にだけマッチしたい場合はそれぞれのオーバーロードを用意する必要がある点です。もちろん、テンプレートを使えば簡単に実現できますが、それは本ガイドの記述の範囲外とします。
戻り値の指定の仕方
戻り値の値返しの基本
すでに例の中でさんざん使用してしまっていますが、戻り値にもムーブが使えます。というか、戻り値ではコピーよりもムーブが優先されます。具体的に言うと、戻り値では左辺値を書いても右辺値のように扱われるため、std::move
を書く必要がありません。return
文の中の式は常にその関数の中の最後の式になるため、破壊してもいいよという感覚なのでしょう。
MovableObj function(){
MovableObj mc;
return mc; // ムーブが優先される
}
ムーブできないオブジェクトの場合、コピーを試みます。(ムーブもコピーもできないオブジェクトはスコープから出られないのでコンパイルエラー、ですね)
RVOとNRVO
ただ、ここまでで説明をやめると、四方八方から大量に矢が飛んできます。「RVOとかNRVOがあるだろ!」って。。。これは基礎知識として知っておいたほうがいい気がするので説明します。(かったるそうだなと思ったらこのセクションは一旦読み飛ばしてもらっても大丈夫です)
実は先ほどの例で「ムーブが優先される」と曖昧に書いたのですが、このまま実装するとコピーもムーブもしないコードを生成するコンパイラが多いです。その理由がRVO、NRVOです。まず、RVOから説明します。
RVO(Return Value Optimization)はざっくりいうと「右辺値を値返しするコードを書いたときに、呼び出し元が確保した領域にオブジェクトを構築する仕組み」です。
MovableObj function()
{
return MovableObj(); // <- この右辺値が確保されるのは呼び出し元のメモリ領域
}
int main()
{
MovableObj mc = function(); // <- 最初からここにオブジェクトが割り当てられる
}
MovableObj
を main
側に確保すればコピーもムーブもいらないから速いよね。当然じゃん。
C++17以降、RVOはコンパイラのサポートが必須とされました。だから大体のコンパイラはRVOベースでコードを吐いてくれると思います。なお、RVOが効いた場合、コピー/ムーブ系の処理の実装がなくてもコンパイルが通るようです。
この考え方を左辺値にも適用するのがNRVO(Named Return Value Optimization)で、「左辺値を値返しするコードを書いたときに、呼び出し元が確保した領域にオブジェクトを構築する仕組み」です。
MovableObj function()
{
MovableObj m; // <- NRVO有効時、この左辺値が確保されるのは呼び出し元のメモリ領域
return m;
}
int main()
{
MovableObj mc = function(); // <- NRVO有効時は最初からここにオブジェクトが割り当てられる
}
が。NRVOはRVOと違ってコンパイラのサポートが必須とされていません。というか、そもそもその性質上、常に適用できるわけではありません。
MovableObj function()
{
MovableObj m1, m2; // <- m1 と m2 どちらに NRVO を適用していいかがオブジェクトの生成時点で決まらない!
if(some_condition())
{
return m1;
} else {
return m2;
}
}
int main()
{
MovableObj mc = function();
}
NRVOはコレクションの構築のときなどにとても好都合だと思いますが、常に有効なわけではないので、「ここはムーブになっちゃうかもなー、まぁでも、ムーブになってもいいか」くらいの感覚で使うと良いと思います。
#include <map>
std::map<int,std::string> createMap()
{
std::map<int,std::string> m;
// 何かすごーく複雑な初期化をやるつもり
return m; // mはNRVOかムーブで返却される
}
int main()
{
std::map<int,std::string> m = createMap();
}
RVOやNRVOを最初から仮定したコードは読む側のリテラシーの高さをも仮定することにもなります。
ModernC++時代の動的な変数アロケーション
ムーブセマンティクスについて、ざっくり説明が終わりました。土台が固まったので、ModernC++時代の変数のアロケーションの考え方について説明していきます。
ModernC++では基本的にアプリケーションコードに new
キーワードを書くことはありません。次の4パターンのいずれかを使ってアロケーション/構築を行います。
-
ローカル変数として構築する
-
make_unique
を使用して構築する -
make_shared
を使用して構築する -
ターゲットのオブジェクトの中に構築する
変数アロケーションの選択肢
ローカル変数として構築する
オブジェクトを構築するのであれば、ローカル変数として構築するのが第一の選択肢です。
MovableObj function(){
MovableObj mc;
return mc;
}
ローカル変数として宣言しても、ムーブのコストが小さいのであればそのままあちこちに引き回せることになります。
代表的な動的構造である std::string
や std::vector
、std::map
のようなオブジェクトはムーブのコストが小さいのでそのまま引き回すことになります。
代入やオブジェクトの引き渡しが発生した際に、オブジェクトがムーブされたのかコピーされたのか確実に把握して有効なデータのありかを管理しましょう。
make_unique で構築する
ムーブできなかったり、ムーブの効率の悪いオブジェクトを引き回したいときは unique_ptr
を使って引き回しをすればよいでしょう。
かつて new
で行っていたオブジェクト生成は unique_ptr
を生成する make_unique
関数で代替するのが第一候補になります。実行効率的には生のポインタをハンドリングすることとほぼ同等とされています。
std::unique_ptr<ImmobilizedObj> foo(){
return std::make_unique<ImmobilizedObj>(100);
// 内部では動的な領域が確保され、ImmobilizedObj(100)相当の初期化が実施される
}
unique_ptr
で確保したオブジェクトを明示的に delete
する必要はありません。他のポインタを代入して参照が切れたり、 unique_ptr
ポインタの寿命が切れた(unique_ptr
のデストラクタが呼ばれた)際にオブジェクトを自動開放してくれます。スコープ外にオブジェクトを持ち出したければ unique_ptr
自体をスコープ外に持ち出すことで寿命を延ばせます。
unique_ptr
はコピーに対応せず、ムーブのみに対応しています。代入を行えますが、常にムーブ代入が行われるため、代入元の unique_ptr
は無効になり nullptr
相当に初期化されます。これにより、unique_ptr
に対してはその名の通り、単一性を確保されます。つまり、make_unique
で生成されたオブジェクトと unique_ptr
は常に一対一の関係にあります。同一のオブジェクトが複数の unique_ptr
の管理下に入ることはありません。unique_ptr
の管理対象から外れたオブジェクトはデストラクタが呼ばれ領域は開放されます。
unique_ptr
が管理するオブジェクトにアクセスする場合は通常、->
演算子を経由します。このあたりは旧来のポインタに似ています。
unique_ptr
から *
演算子により参照を取り出すこともできます。これにより unique_ptr
自体を受け渡すことなく、効率よく処理を移譲できますが、管理下のオブジェクトの生存期間が確実に確保されている状況でのみ実施すべきです。
下の例では foo
関数で生成されたオブジェクトを boo
関数が受け取ったあとは boo
関数が unique_ptr
を保持し、bar
関数に渡すことなく握り続けています。つまり、f
の生存期間を boo
関数が管理しているということです。 このような状況を「 boo
が f
の所有権を握っている」などと表現したりします。
std::unique_ptr<ImmobilizedObj> foo(){
return std::make_unique<ImmobilizedObj>(100);
}
void bar(ImmobilizedObj& m){
// なにか処理
}
void boo(){
auto f = foo(); // fはfooからムーブされる
std::cout << f->getNum() << std::endl; // fのメンバ値を出力
bar(*f); // オブジェクトの生存期間が確保できていれば参照渡ししても良い
// ここでやっとImmobilizedObjのデストラクタが呼ばれる
}
次の例では親オブジェクトである ParentObj
に unique_ptr
を保持させる例を挙げます。ParentObj
のデストラクタで unique_ptr
のデストラクタが呼ばれ、その処理の中で ImmobilizedObj
のインスタンスについてもデストラクタコールされます。
std::unique_ptr<ImmobilizedObj> foo(){
return std::make_unique<ImmobilizedObj>(100);
}
class ParentObj
{
private:
std::unique_ptr<ImmobilizedObj> ptr = nullptr;
public:
void setImmobilized(unique_ptr<ImmobilizedObj> && ptr){
// unique_ptrを受け取るときはムーブなので右辺値参照で受けるのが行儀が良い
this.ptr = std::move(ptr); // ムーブ代入
}
~ParentObj()
{
this.ptr = nullptr; // unique_ptrのデストラクタが呼び出される
// 実は明示的にnullptrを代入しなくても~ParentObjの最後でデストラクタが呼ばれる
}
};
void boo(){
auto f = foo(); // fはfooからムーブされる
{
ParentObj p;
p.setImmobilized(std::move(f)); // fはpに対してムーブされる
// fはもはやムーブ済みでnullptr相当
// fのデストラクタはpのデストラクタ経由で呼び出される
}
// fのスコープ内ではあるがfooが生成したImmobilizedObjのオブジェクトはこの時点ではすでに存在しない
}
覚えたばかりのムーブでポインタをハンドリングするのは多少苦しむかもしれませんが、unique_ptr
の使用は基本的なイディオムになるので積極的に使用して慣れてゆくのが良いと思われます。
make_shared で構築する
unique_ptr
は実行効率は良いのですが、所有者が一つに限られるため、複数のオブジェクトから参照されるような構造には向きません。このような場合に対応するため、C++では参照カウンタを使用した共有ポインタ shared_ptr
を用意しています。
基本的な使用方法は unique_ptr
とほぼ同じですが、 shared_ptr
にはコピー系の演算も実装されており、コピーするたびに参照数を増加させ、shared_ptr
のインスタンスのデストラクタが呼ばれるたびに減少させます。オリジナルや複製されたすべての shared_ptr
が破棄されたとき、管理対象のオブジェクトのデストラクタを呼び、領域を開放します。
とても便利ですが unique_ptr
よりは実行効率が劣るとされます。
std::shared_ptr<ImmobilizedObj> foo(){
return std::make_shared<ImmobilizedObj>(100);
}
class ParentObj
{
private:
std::shared_ptr<ImmobilizedObj> ptr = nullptr;
public:
void setImmobilized(const shared_ptr<ImmobilizedObj> & ptr)
{
// shared_ptrをコピーするのであれば左辺値参照で受ける、
// ムーブするのであれば右辺値参照で受けるのが行儀
this.ptr = ptr; // ここでshared_ptrがコピーされ、参照数が増える
}
~ParentObj()
{
this.ptr = nullptr; // shared_ptrのデストラクタが呼び出される
// 実は明示的にnullptrを代入しなくても~ParentObjの最後でデストラクタが呼ばれる
}
};
void boo(){
auto f = foo(); // fはfooからムーブされる
{
ParentObj p1, p2;
p1.setImmobilized(f); // fをp1に対してコピー
p2.setImmobilized(f); // fをp2に対してコピー
f = nullptr; // 全部で3つのコピーができたが、オリジナルはいらないので破棄
}
p1.setImmobilized(nullptr);
// この時点ではImmobilizedObjのインスタンスは生きている
p2.setImmobilized(nullptr);
// すべてのshared_ptrのインスタンスが消えたので
// ImmobilizedObjのデストラクタが呼ばれ破棄される
// ここでfのスコープが切れるが、shared_ptrもImmobilizedObjも残っていない
}
ターゲットオブジェクトの中に構築する
大まかなアロケーション方法については説明してきたのですが、他のオブジェクトに含まれるオブジェクトを構築する方法について説明していませんでした。これについて少し説明します。
たとえば、コンテナなどの構造を利用する際にはコンテナに含まれるオブジェクトを初期化する必要があります。ひとたびコンテナ側にオブジェクトを構築すれば、あとの開放処理などはコンテナ側で呼び出してくれます。
コンテナの初期化を例にとって初期化のパターンを見てみましょう。
デフォルト構築する
配列などの初期化でよく発生するパターンです。デフォルトコンストラクタで構築してしまうので、その後改めてデータを上書きする必要があることが多いです。
std::unique_ptr<MovableObj[]> mv;
void initializer(std::size_t size)
{
mv = std::make_unique<MovableObj[]>(size); // この時点で配列要素はデフォルトコンストラクタで初期化
for(int i = 0 ; i < size ; i++){
mv[i].setString("I am " + i + "th Object!"); // 追加で初期化処理を行う
}
}
コピー構築する
初期化に使用するオブジェクト自体を使いまわしたかったり、コピーコストが安いときの手抜きとして使用するときに使用する手段です。
左辺値参照版のコレクション追加関数 std::vector::push_back(const T&)
はデータをコピーして格納します。
std::vector<MovableObj> mv;
void initializer(std::size_t size)
{
for(int i = 0 ; i < size ; i++){
MovableObj m;
m.setString("I am " + i + "th Object!"); // 何やら動的な初期化をしてみた
mv.push_back(m); // push_back(const T&)は左辺値参照を受け取りコピーして格納
// この時点でもmは意味のあるデータを格納している
}
}
ムーブ構築する
複雑なオブジェクトを入れ込みたいときに通常使用する方法です。汎用性が高くコピーコストを回避しながら複雑な初期化を行えます。ただし、ムーブコストも高いようなオブジェクトにはこの方法は向きません。
右辺値参照版の追加関数 std::vector::push_back(T&&)
は右辺値参照を受け取り、ムーブしてデータを格納します。
std::vector<MovableObj> mv;
void initializer(std::size_t size)
{
for(int i = 0 ; i < size ; i++){
MovableObj m;
m.setString("I am " + i + "th Object!"); // 何やら動的な初期化をしてみた
mv.push_back(std::move(m)); // push_back(T&&)は右辺値参照を受け取り内部でムーブする
// この時点でmは破壊済みでありデータ上の意味を持たない
}
}
unique_ptr
/ shared_ptr
をムーブして構築する
直接ムーブできない、ムーブ効率の悪いオブジェクトの場合、unique_ptr
/ shared_ptr
でラップしてムーブする方法もあります。unique_ptr
についてはコンテナから直接取り出してしまうとコンテナ側の unique_ptr
が nullptr
になってしまうので注意する必要があります。
unique_ptr
をムーブする例std::vector<std::unique_ptr<ImmobilizedObj>> iv;
void initializer(std::size_t size)
{
for(int i = 0 ; i < size ; i++){
auto io_ptr = std::make_unique<ImmobilizedObj>("I am " + i + "th Object!"); // 何やら動的な初期化をしてみた
iv.push_back(std::move(io_ptr));
}
}
// 参照なら取得できる。
ImmobilizedObj& getImmobilizedObj(std::size_t index)
{
return *(iv[index]);
}
// unique_ptr自体を取り出すとvectorの外側にムーブされてしまうのでこれは危険
// std::unique_ptr<ImmobilizedObj> getImmobilizedPtr(std::size_t index)
// {
// return iv[index];
// }
直接構築する
コンテナの中には直接構築といってコンストラクタ引数を渡すと中間のアロケーションをせずコンストラクタを呼び出して構築してくれるものがあります。ムーブコストすら回避できる性能劣化の少ない初期化の方法です。ただし、対象のコレクションが直接構築をサポートしている必要があります。
直接構築を使う場合、ライブラリのI/F記述やシグニチャを確認して直接構築の機能があるかを確認することがまず第一歩になります。たとえば、 std::vector
には emplace_back
というメンバー関数があります。
std::vector<MovableObj> mv;
void initializer()
{
for(int i = 0 ; i < 19999 ; i++){
MovableObj& m = mv.emplace_back("I am " + i + "th Object!");
// MovableObj("I am " + i + "th Object!")相当の処理で直接構築された
m.setAdditionalAttribute(i); // C++14以降のemplace_backは参照を返すので追加で初期化処理することも可能
}
}
#個人的にはムーブ構築を基本として直接構築があればそれを使うか〜、くらいの感覚でいます。
アロケーション選択方法まとめ
アロケーション、初期化の手段の選択について大まかな判断チャートを作ってみました。
-
ローカルでライフサイクル管理可能 → ローカルで宣言
-
ライフサイクルがスコープ外に出る
-
共有不要でムーブコストが小さい → ローカルで宣言してムーブで管理する
-
共有不要でムーブコストが大きい → unique_ptrを使用する
-
共有が必要 → shared_ptrを使用する
-
-
親になるオブジェクトがある
-
共有不要でムーブコストが小さい → 親オブジェクト内に直接構築
-
共有不要でムーブコストが大きい → 親オブジェクト内にunique_ptrで保持
-
共有が必要 → 親オブジェクト内にshared_ptrで保持
-
アロケーションされた変数の引き回し方
アロケーション後のオブジェクトをどのように引き回すかについて簡単に説明します。
コピーして引き回す
コピーが可能で、ロジック上複製しても問題なく、かつコピーしても効率の良いものはコピーして引き回すと良いでしょう。 深いことは考えなくてよくなります。ただ、あまりこういうのが歓迎されるシチュエーションは少ないのではと思います。。。
LightObj getLightObj()
{
return LightObj; // 戻り値もコピーで返しちゃえ
}
void eatLightObj(LightObj lo)
{
// コピーして何かする
}
int main()
{
LightObj lo = getLightObj();
eatLightObj(lo);
return 0;
}
参照を引き回す
呼び出し元となる関数がオブジェクトの生存期間を管理できているなら、参照で引き回すのが効率が良いです。この場合、呼び出された側は参照を保持し続けたり非同期処理に受け渡すなどは基本的にNGだと考えるべきです。呼び出された側が左辺値参照で渡された引数をムーブするなんてのももちろん避けるべきです。
void eatNormalObj(NormalObj& no)
{
// データを使って何かする
// 左辺値参照でうけとったということは壊していい約束ではないので
// ムーブは絶対にしない
}
int main()
{
NormalObj no;
eatNormalObj(no); // 確実に生存範囲内
return 0;
}
ムーブして引き回す
関数間で所有権を引き回すときにムーブで引き回すのが効率が良いことがあります。右辺値参照や戻り値を使ってムーブをつないでいくイメージになります。この場合でも生存期間を所有している関数がライフサイクルを管理できるのであれば、その期間内では左辺値参照を使って引き回すことが可能です。
MovableObj foo(){
rerurn MovableObj(100);
}
void bar(MovableObj& m){
// なにか処理
}
void boo(){
auto m = foo(); // fはfooからムーブされる
bar(m); // オブジェクトの生存期間が確保できていれば参照渡ししても良い
// ここでやっとImmobilizedObjのデストラクタが呼ばれる
}
unique_ptr
を引き回す
上記のムーブのイメージに近いですが、 unique_ptr
で宣言されたものはオブジェクト自体をムーブする代わりに、 unique_ptr
をムーブして引き回すことになります。ムーブで引き回している場合同様、生存期間を所有している関数がライフサイクルを管理できるのであれば、その期間内では左辺値参照を使って引き回すことが可能です。
unique_ptr
を引き回す例std::unique_ptr<ImmobilizedObj> foo(){
return std::make_unique<ImmobilizedObj>(100);
}
void bar(ImmobilizedObj& m){
// なにか処理
}
void boo(){
auto f = foo(); // fはfooからムーブされる
std::cout << f->getNum() << std::endl; // fのメンバ値を出力
bar(*f); // オブジェクトの生存期間が確保できていれば参照渡ししても良い
// ここでやっとImmobilizedObjのデストラクタが呼ばれる
}
shared_ptr
を引き回す
shared_ptr
は unique_ptr
と異なり、コピーに融通が効くので適当に管理したくなります。しかし、頻繁にコピーを繰り返すことは性能劣化の要因になりえます。shared_ptr
自体の参照を渡したり、ムーブするなどして必要最小限のコピーにとどめるべきだとされます。また、ムーブや unique_ptr
で引き回している場合同様、生存期間を所有している関数がライフサイクルを管理できるのであれば、その期間内では左辺値参照を使って引き回すことが可能です。
std::shared_ptr<ImmobilizedObj> foo()
{
return std::make_shared<ImmobilizedObj>(100);
}
void bar(ImmobilizedObj& m)
{
// なにか処理
}
class ParentObj
{
private:
std::shared_ptr<ImmobilizedObj> ptr = nullptr;
public:
void setImmobilized(const shared_ptr<ImmobilizedObj> & ptr)
:ptr(ptr)
{
}
void someOperation()
{
// ここではImmolizedObjのインスタンスの所有権を確実に握れているので
// 参照で引き回せる
bar(*(this->ptr));
}
~ParentObj()
{
this.ptr = nullptr;
}
};
void boo()
{
ParentObj p1, p2;
{
auto f = foo(); // fはfooからムーブされる
p1.setImmobilized(f); // fをp1に対してコピー
p2.setImmobilized(f); // fをp2に対してコピー
}
p1.someOperation();
p1.setImmobilized(nullptr);
// この時点ではImmobilizedObjのインスタンスは生きている
p2.someOperation();
p2.setImmobilized(nullptr);
// すべてのshared_ptrのインスタンスが消えたので
// ImmobilizedObjのデストラクタが呼ばれ破棄される
// ここでfのスコープが切れるが、shared_ptrもImmobilizedObjも残っていない
}
setImmolibizedObj
の引数は shared_ptr
への参照になっていますが、この実装方法には2つの考慮があります。一つは shared_ptr
の不用意な複製を防ぐ意味です。もう一つは、ムーブでの受け渡しが基本である unique_ptr
の引き回しの記述とのバランスを取る意味です。
その他、ModernC++っぽい言語要素
ムーブセマンティクスとオブジェクト構築や引き回しの方法を説明したので、だいたいこれでModernC++としてはOKなのですが、「こういう言語機能を意識しているとナウいよ!(あえてモダンと言わない)」というシャレオツ言語機能について軽く説明していきます。
それぞれ、ざっと流し読んでもらえれば、という感じです。コードを書くときにふと思い出して使ったり、人のコードを読んでいるときに「あ、これ、xxxで出たやつだ!」となるように情報を羅列するだけです。
auto推論
ここまでにすでに多用していますが、 auto
を書くことで型宣言を省略できます。 auto
は基本的に「推論対象から参照を取り払った型」を示します。
ローカル変数には型指定をしたい場合を除いて auto
を使うのが基本になります。
void foo()
{
auto v = std::vector<int>{...};
auto f = foo();
}
関数戻り値を auto
にすることも可能です。ただし、省略することでシグニチャが読みづらくなることもあるので、使用の是非には議論があると思います。
auto foo1()
{
return std::make_unique<FooType>();
}
メンバー変数やグローバルなコンテキストでの変数に auto
を使うことは通常、ないと思います。
戻り値型の後置
auto
の説明をしたので、ついでに戻り値型の後置も説明しておきます。auto
キーワードで戻り値型を省略したあと、->
を付与することで戻り値型を後置することができます。
auto foo2() -> std::unique_ptr<FooType>
{
return std::make_unique<FooType>();
}
まぁ、これはテンプレートを書くときに使う機能なので、当面使うことはないかも知れないですね。
auto foo3() = []() -> std::unique_ptr<FooType> {
return std::make_unique<FooType>();
}
constexpr / nullptr
constexpr double pi_div_2 = std::numbers::pi / 2.0;
// const double pi_div_2 = std::numbers::pi / 2.0; は適切でない
また、同じようなノリで NULL
を使わずに nullptr
を使用するのが正しい作法になりました。これもすでに多用していますね。nullptr
は今やほとんど使用しない生ポインタに対しても使えるほか、 unique_ptr
や shared_ptr
に対しても使えます。
char* raw_ptr = nullptr;
unique_ptr<int> u_ptr = nullptr;
shared_ptr<int> s_ptr = nullptr;
イテレータと範囲for
#include <iostream>
#include <vector>
struct Obj
{
public:
int value;
Obj(int i)
:value(i)
{
}
};
std::vector<Obj> v{Obj(1),Obj(2),Obj(3)};
int main()
{
for(std::vector<Obj>::iterator it = v.begin();it!=v.end();it++) // めんどくさすぎるでしょ。。。
{
std::cout << it->value << std::endl;
}
return 0;
}
範囲forはループの記述を大幅に改善します。
int main()
{
for(const auto& item : v) // ループ構文がメチャクチャ簡単!
{
std::cout << item.value << std::endl;
}
}
お約束として、範囲forでイテレータを受ける型の記述はコンテナに含まれる値の型に対して、おおまかに次のようになります。
-
プリミティブ型 →
const auto
かauto
-
クラスオブジェクト
-
const auto&
→ リードオンリーのとき -
auto&
→ 対象のオブジェクトを書き換えるとき/コンテナ側からデータをムーブアウトするとき
-
まぁ、だいたい関数の引数と同じですね。データをコンテナからムーブしてしまっていいかどうかはコンテナや対象のデータの特性を確認する必要があります。
using
おっさんは using
と言われると有名な575の句である using namespace std;
を思い浮かべると思うのですが、ModernC++では using
の主な用途は typedef
に相当する機能で、型エイリアスなどと呼称します。
using str_gen_func = std::string (*)(int); // typedef std::string (*str_gen_func)(int); とほぼ同じ
その他、どうしても名前空間を外したいものをピンポイントで名指しします。
#include <iostream>
#include <chrono>
int main()
{
// cout , endlだけは名前空間外したい
using std::cout, std::endl;
// ユーザ定義リテラルの名前空間外し:後述します
using std::literals::chrono_literals::operator"" h;
// :
}
てか、基本的に名前空間を using
したりしません。std
とかは std::string
みたいな感じで、名前空間つけたまま使っちゃいます。まどろっこしいパッケージ名に対しては名前空間をパカッとおっぴろげにするのではなく、短い名称に割り付けなおしたりします。
namespace fs = std::experimental::filesystem;
if(fs::create_directory("new_directory") != true){
// ....
}
override
/ delete
/ default
class
/ struct
のメンバ関数の構成に関するキーワード群です。
override
virtual
関数を使わざるを得ない場合、オーバーライドする側の class
/ struct
定義に override
をつけておくと実装漏れをエラーで検出してくれます。
class MyBaseClass
{
public :
virtual void foo() = 0;
}
class MyClass : public MyBaseClass
{
virtual void foo() : override;
}
// MyClass::foo() の実装がなければエラー
noexcept
noexcept
を関数のシグニチャにつけると例外を発生しない旨をコンパイラに伝えられるのでより最適化されたコードを生成できます。また、noexcept
のシグニチャがついた関数だけで noexcept
関数を構成すれば安心です。ただし、noexcept
を指定したにもかかわらず例外を発生すると std::terminate()
が即座に呼ばれるということなので計画的なご利用が必要です。
class NoExceptClass
{
void foo() noexcept;
}
「オブジェクト指向言語」の隆盛とともに鳴り物入りで登場した例外機構ですが、時代を経てその管理の厄介さや性能面での問題が明確に認識されてきています。noexcept
キーワードはそのような時流を受け、出るべくして出てきた言語機構であるとも言えそうです。
ラムダ式とキャプチャ
コールバックなどを要求する関数に、名前のついた関数を渡す代わりに匿名の関数であるラムダ式を直接渡せるのは本当に便利です。その場にロジックを書けることでコードの可読性を大幅に向上できることがあります。
std::for_each()
関数の引数として渡す例を示します。
std::vector<int> v;
std::for_each( v.begin(), v.end(), [](auto i){
std::cout << i << ",";
});
ラムダ式は std::for_each()
に代表されるイテレーション系の処理のほか、非同期処理、コールバックなどに多用されます。これらの機能では関数だけではなく、周辺環境(コンテキスト)の情報ごと渡したいことが多く、関数ポインタだけでは不満なことが多いのでした。
ラムダは呼び出し元のコンテキストを「捕まえる」ためにキャプチャという機能を持っています。ラムダを活用するにはこのキャプチャについて理解しておく必要があります。
参照キャプチャ
ラムダ式側から現在のコンテキストに対する参照を掴ませる指定です。最初の []
に &
を指定しておくとラムダからコンテキストをいじるときに参照経由でアクセスします。
std::vector<int> v;
int count = 0;
std::for_each( v.begin(), v.end(), [&](auto i){
std::cout << i << ",";
count++; // countへの参照にアクセスしている
});
std::cout << "total count is :" << count << std::endl;
ただ、非同期処理やコールバックを設定する場合、ローカル変数への参照を掴ませるとか恐ろしくてできないですね。
コピーキャプチャ
次の例では、同期処理の例となっていますが、コピーキャプチャは非同期処理などで現在のコンテキストからラムダ式にデータを受け渡したいときに便利です。
std::vector<int> v;
int offset = 3;
std::for_each( v.begin(), v.end(), [=](auto& i){
i += offset; // offsetがラムダのコンテキストにコピーされている
});
コピーキャプチャされた値には暗黙に const
修飾されるので基本的に書き込めません。あまり書き込む用事もないと思いますが書こうとして驚かないで。。。
初期化キャプチャ
キャプチャ時にムーブすることもできます。初期化キャプチャという機能を使います。これは非同期処理にオブジェクトを渡したいときに便利です。変数名を指定して受け渡しの処理を指定することができます。
int main()
{
MovableObj1 m1;
auto f = std::async( std::launch::async, [m1 = std::move(m1)](){
MovableObj2 m2;
// なにかMovableObj(m1)を使った処理
return m2;
});
auto m2 = f.get(); // ちなみにm2の型はMovableObj2
}
初期化キャプチャを使えば参照、コピー、ムーブなど自由自在です。
std::vector<int> v;
MovableObj m;
CopiableObj c;
ReferencableObj r;
std::for_each( v.begin(), v.end(), [m=std::move(m),c=c,&r=r](auto i){
m; // mはムーブされている
c; // cはコピーされた
r; // rは参照である
});
キャプチャについては初期化キャプチャを使えば他に何も考えなくていいじゃん、と思いますが、他にも this
キャプチャなど細かい仕様があります。細かいことを説明すると長くなるので気になった方は cppreference などの参考文献でも見てください。
リテラル
コードを読んでいると、リテラル関係の記述もそこそこ多く感じられますので簡単にだけ触れます。
メンバ関数の修飾
これは別に新しい機能というわけでもないのですが、メンバ関数に対して実質的な第一引数である *this
に制約をかけることができます。const
や volatile
などの制約が可能でしたが、左辺値、右辺値の区別も行えるようになっています。
class X
{
public:
void foo_1 (int i)
{}
void foo_2 (int i) const
{}
void foo_3 (int i) & // 左辺値のときだけ c.f. static foo_3s(X& x,int i)
{}
void foo_4 (int i) && // 右辺値のときだけ c.f. static foo_3s(X&& x,int i)
{}
};
X lv;
lv.foo_3(0); // OK
// lv.foo_4(0); // NG
// X().foo_3(0); // NG
X().foo_4(0); // OK
便利なライブラリ機能
ここまで来ると言語機能的にはもう相当にシャレオツなのですが、その状態でC言語も使わないのに #include <string.h>
とかしてしまうともう本当に見てられないので、こういうライブラリ機能を使えばいいよ!というのをピックアップして紹介してみます。
この辺はもう、タイトルをみて気になったものを眺めてもらうだけでいいと思います。
コレクション
動的にサイズが決定するような配列はほとんど使わず、std::vector
を使うのが基本なので、これで運用していきましょう。想定する大きさがあれば std::vector::reserve()
で予め大きめの領域を確保して作業しましょう。こうすることでリアロケーションによる実行コストを回避できます。
std::vector<int> v;
v.reserve(10);
for(int i=0;i<5;i++)
{
v.push_back(i);
}
集合やハッシュマップの概念は std::set
/ std::map
があります。もう説明するだけ無駄かも知れませんが、順序や重複可否などのバリエーションを静的に設定できるのがC++の良さでもあります。<set>
/ <map>
あたりの仕様を確認してから使い始めると良いでしょう。
拡張forも使えて便利です。
#include <set>
#include <iostream>
std::set<int> s;
void print_the_set()
{
for(const auto& item : s)
{
std::cout << "item : " << item << std::endl;
}
}
bool has_a_member(int item)
{
// return s.contains(item); // これはc++20以降
return s.find(item) != s.end();
}
map
についても範囲for文が使えますがこれについてはpair
/ tuple
のセクションを見てください。
pair
/ tuple
2つ、あるいは3つ以上のデータをセットで扱う仕組みです。他のプログラミング言語では当たり前にあったものがC++には長く存在しませんでした。 std::make_pair
/ std::make_tuple
を使用して構築したり、型推論が効いているところでは初期化リスト {}
を使って構築することも可能です。
#include <tuple>
std::pair<int,std::string> p1{2,"aaa"};
auto p2 = std::make_pair(2,"aaa");
std::pair<int,std::string> get_p3()
{
return {2,"aaa"};
}
std::tuple<int,std::string,std::string> t{3,"aaa","bbb"};
auto t2 = std::make_tuple(3,"aaa","bbb");
std::tuple<int,std::string,std::string> get_t3()
{
return {3,"aaa","bbb"};
}
C++17から pair
/ tuple
を簡単に分解する言語機構が使えるようになりました。構造化束縛といいます。値としての束縛も、参照としての束縛もできます。
auto [k1,v11] = get_p3(); // 値として束縛
auto t3 = get_t3();
auto& [k2,v21,v22] = t3; // 参照として束縛
構造化束縛によって map
の範囲for文が書きやすくなりました。
std::map<int,std::string> a_map {
{3,"aaa"},{2,"bbb"}
};
for(auto const& [key, value] : a_map)
{
std::cout << "key is : " << key << " value is : " << value << std::endl;
}
構造化束縛によって、やっと pair
/ tuple
を便利に使える土台ができたという感じがします。ただし、構造化束縛は今のところネストできません。また、値で束縛するか参照で束縛するかを項目ごとに選ぶこともできません。
文字列
このガイドの執筆時点での文字列の標準的な型は std::string
つまり std::basic_string<char>
です。なので、当面はこれを基本的な文字列の型として使用することになるでしょう。(Windows環境だと std::wstring
が標準になりますが、、、)
C++20以降は std::u8string
つまり std::basic_string<char8_t>
が主流になっていくものと思われます。これらは依存ライブラリなどの互換性を考慮して決めていくことになると思われます。(*)
また、C++17で部分文字列など、リソースをシェアした文字列の考え方として std::string_view
が導入されています。部分文字列を引き回したりする場合には std::string&
を使うより、std::string_view
を使うほうが便利かも知れません。また、 std::string&
から std::string_view
には暗黙のキャストがあるので、関数のインターフェースに std::string_view
を使用すると std::string
でも std::string_view
でも使用できるようになります。
std::string s1 = "string";
char s2[] = "char array";
void print(std::string_view sv)
{
// coutはstring_viewを受け付けるのでこれ自体は意味ないが
std::cout << sv << std::endl;
}
int main()
{
print(s1);
print(std::string_view(s1));
print(s2);
}
なお、string_view
についてもC++20以降は std::string
同様に std::u8string_view
に移行していくものと思われます。
正規表現
#include <iostream>
#include <regex>
int main()
{
const char s[] = R"(She said "He is hungry.")";
const std::regex re(R"("(\w+) is (\w+).")");
std::cmatch m;
if (std::regex_search(s, m, re))
{
for (std::size_t i = 0, n = m.size(); i < n; ++i)
{
std::cout << i << ":'" << m.str(i) << "\', "
<< "position = " << m.position(i) << ", "
<< "length = " << m.length(i) << std::endl;
}
}
}
0:'"He is hungry."', position = 9, length = 15 1:'He', position = 10, length = 2 2:'hungry', position = 16, length = 6
ただし、2021年時点においても、非ASCII文字に対しては標準ライブラリの実装はロクに動かないことが多いようです。筆者も SRELL(*) という互換ライブラリを使用することが多いです。
SRELLのサイトのサンプルをそのまま引用します。ほぼ、標準のregexと使い方が同じだとわかると思います。
// Example 01:
#include <cstdio>
#include <string>
#include <iostream>
#include "srell.hpp"
int main()
{
srell::regex e; // 正規表現オブジェクト。
srell::cmatch m; // 結果を納めるオブジェクト。
e = "\\d+[^-\\d]+"; // 正規表現をコンパイル。
if (srell::regex_search("1234-5678-90ab-cdef", m, e))
{
// printfを使うなら。
const std::string s(m[0].first, m[0].second);
// 上は下のどちらかでも良い。
// const std::string s(m[0].str());
// const std::string s(m.str(0));
std::printf("result: %s\n", s.c_str());
// iostreamを使うなら。
std::cout << "result: " << m[0] << std::endl;
}
return 0;
}
std::u8string
が当たり前になった世界では標準ライブラリの正規表現もきちんと動くようになると思われます。
時刻
<chrono>
ヘッダのライブラリを使用すると現在時刻や時刻差分が扱えます。プラットフォーム非依存で時刻取得が行えます。一般的な時刻管理の用途であればこれで十分でしょう。
#include <chrono>
#include <iostream>
// この一行を書くのにちょっと憂鬱になるというのは内緒
using std::literals::chrono_literals::operator"" h;
int main()
{
auto now = std::chrono::system_clock::now();
auto after_4_5 = std::chrono::time_point_cast<std::chrono::minutes>(now + 4.5h);
std::time_t t0 = std::chrono::system_clock::to_time_t(now);
std::cout << "current time is " << std::ctime(&t0) << std::endl;
std::time_t t1 = std::chrono::system_clock::to_time_t(after_4_5);
std::cout << "after 4.5h'll be " << std::ctime(&t1) << std::endl;
}
current time is Wed Mar 10 23:05:44 2021 after 4.5h'll be Thu Mar 11 03:35:00 2021
ファイルシステム
ファイルシステムを取り扱う標準ライブラリ機能はこれまで貧弱、というかありませんでした。C++17以降、std::filesystem
が導入されて、ディレクトリ操作も含めた互換性の高いファイルシステム操作が行えるようになりました。これはクロスプラットフォームで開発するときにはとてもありがたい機構で、これまでPOSIXやWindowsのAPIラッパを書いていた方々がメンテナンスから開放される日も近いのではと思います。
#include <iostream>
#include <filesystem>
#include <fstream>
namespace fs = std::filesystem;
int main()
{
const std::string dir0("dir0"), dir1("dir1");
const std::string file0("file0.txt"), file1("file1.txt");
constexpr auto& SEP = fs::path::preferred_separator;
// dir0/file0.txt を作成
fs::create_directory(dir0);
std::ofstream(dir0 + SEP + file0);
// dir0/dir1/file1.txt を作成
fs::create_directory(dir0 + SEP + dir1);
std::ofstream(dir0 + SEP + dir1 + SEP + file1);
// ディレクトリ内をイテレータで走査する (※型定義はautoだが、明示するためにあえて記述)
for (const fs::directory_entry& x : fs::directory_iterator(dir0)) {
if(x.is_regular_file())
{
std::cout << x.path() << " is a regular file." << std::endl;
}
else if(x.is_directory())
{
std::cout << x.path() << " is a directory." << std::endl;
}
else
{
std::cout << x.path() << " is an unknown entry." << std::endl;
}
}
}
"dir0/file0.txt" is a regular file. "dir0/dir1" is a directory.
#include <experimental/filesystem>
namespace fs = std::experimental::filesystem;
if(fs::create_directory("new_directory") != true){
// ....
}
async
非同期処理で計算結果を得たいなど、ワンショットで処理を非同期に出したいだけであれば st::async
が便利です。いわゆる「Futureパターン」を提供します。
結果を取得する際に std::future::get()
を使用しますが、参照型と実体型のいずれも使用できます。実体型ではムーブが優先されます。(*)
#include <future>
int main()
{
MovableObj1 m1;
// fの型は std::future<MovableObj2>
auto f = std::async( std::launch::async, [m1 = std::move(m1)](){
MovableObj2 m2;
// なにかMovableObj(m1)を使った処理
return m2;
});
auto& mr2 = f.get(); // ちなみにmr2の型はMovableObj2&
// auto m2 = f.get(); // ムーブ/コピー可能であれば値で受けることも可能。
// ただし、future::get()が呼べるのはひとつのオブジェクトあたり一回のみ
}
(*) ムーブが「優先」というのは std::future::get()
の戻り値が std::move()
が修飾されているのと同等、という意味です。ムーブがあればムーブで、コピーがあればコピーで返され、どちらもなければコンパイルエラーになります。この辺の感覚は普通の戻り値に近いですが、関数の戻り値は一度 future
によって確保され、その後受け渡されることを頭に入れておくと良いと思われます。
mutex
かなり説明を省略するのでこのセクションはPOSIXのpthreadの概念に慣れていないと意味不明かも知れません。興味のない人は読み飛ばしてもらってOKです。
排他処理と条件変数を実現する機能として std::thread
/ std::mutex
/ std::condition_variable
があります。pthreadに相当する機能です。C++ではmutexロックの取得と開放をスコープと紐付けられるので開放漏れにビクビクする必要がありません。
#include <thread>
#include <mutex>
#include <condition_variable>
#include <memory>
#include <optional>
class SynchronizedData {
private:
std::mutex m;
std::condition_variable cv;
bool data_produced = false;
std::optional<MovableObj> mo = std::nullopt;
public:
void produce()
{
// データの準備処理
mo = std::make_optional<MovableObj>();
// mutexでロックを実施(左辺値のスコープ切れと同時にロック開放)
std::unique_lock<std::mutex> lock(m);
data_produced = true;
cv.notify_all();
}
void consume()
{
// mutexでロックを実施(左辺値のスコープ切れと同時にロック開放)
std::unique_lock<std::mutex> lock(m);
// 条件変数でデータの準備ができるまで待機
cv.wait(lock, [this] { return data_produced; });
// データ処理の実施
// mo->...
}
};
int main()
{
SynchronizedData d;
std::thread t1([&] { d.consume(); });
std::thread t2([&] { d.produce(); });
t1.join();
t2.join();
return 0;
}
なお、これだけの例を挙げておいてなんですが、そもそも、この例に挙げているくらいのことしかやらないのであれば前のセクションで紹介した future
の適用を考えるべきです。プロデューサー側のスレッドは std::async
で生成し、コンシューマー側のスレッドに future
をムーブ渡しすればいいのです。std::mutex
とか std::condition_variable
のことを考える必要はありません。
#include <future>
MovableObj produce()
{
MovableObj m;
// データの準備処理
return m;
}
void consume(MovableObj& m)
{
// データ処理の実施
}
int main()
{
SynchronizedData d;
auto f1 = std::async(std::launch::async, [] { return produce(); });
auto f2 = std::async(std::launch::async, [f=std::move(f)] { consume(f.get()); });
// f1はムーブ済みのため、f2のラムダの終了に伴って回収される
f2.wait();
// f2はmainのスコープ切れによって回収される
return 0;
}
もうちょっとだけ細かい、だけどざっくりした話
右辺値参照
右辺値参照をどう捉えるか、というのは本当にとらえどころのない話だと思いますが、普段の使い方としてはやはり、「関数引数におけるマッチングの用途に使う」と考えておくのが妥当そうです。
右辺値参照は「右辺値を束縛し、生存期間を延長する」用途にも使える、過去からある仕様です。
std::vector<int>&& iv1 = {1,3,3}; // 初期化子の寿命は切れるが、ivが生存期間を延長
std::vector<int>&& iv2 = get_some_vector(); // 戻り値の生存期間を延長
しかし、ModernC++ではムーブセマンティクスやRVOなどがあるので、値束縛で十分だとも考えられます。
std::vector<int> iv3 = get_some_vector(); // RVO効いてるならこれで十分
右辺値参照は型のように見えるかも知れませんが、型と考えず、関数呼び出し時のパターンマッチと捉えるのが気楽だと思います。
実際、右辺値参照もすべて左辺値として扱われます。つまり、型の上では T&&
は T&
と同じと扱われます。オーバーロード解決時にパターンマッチの要素として左辺値か右辺値かが問われるだけで、その参照が T&&
として定義されたか、T&
として定義されたかは問題にはなりません。
void foo(int&){}
void foo(int&&){}
void boo(int&& i){
foo(i); // booの内部においてもfoo(int&)とマッチする。
}
int main()
{
int&& i = 3; // iは右辺値参照として定義したが、i自体は左辺値
foo(i); // iは左辺値なので、foo(int&)にマッチする。定義が int&& とされたこととは関係しない。
boo(std::move(i)); // boo(int&&)とマッチするにはstd::moveが必要
return 0;
}
テンプレートの読み書きをしない限り、関数引数、戻り値以外の右辺値参照は必要ないと言って良いでしょう。
std::move
とは実際なんなのか
ここまでの立ち位置では std::move
はただの魔法のキーワードで一体なんなのか、わからないままかも知れません。std::move
がやっていることは、実際には T
や T&
を T&&
にキャストしているだけです。
なぜそれで右辺値になるのか、ということなのですが、、、左辺値とか右辺値といった「値カテゴリ」というものが決まっています。C++17での値カテゴリの分類を右辺値/左辺値の区別の観点からざっくり説明すると下記です。
-
lvalue(左辺値)
-
名前のあるものやその一部を表すもの(一部とは、例えばメンバ変数など)
-
左辺値参照を返す関数(戻り値)/演算子の結果
-
左辺値参照へのキャスト式
-
-
rvalue(右辺値)
この分類をどう理解するか、ということですが、、、もともと左辺値、右辺値の概念があり、そこに参照をどう扱うかという考え方が追加されたというイメージです。追加されたような部分とは、lvalueの左辺値参照関係の仕様とxvalueの右辺値参照関係の仕様ですね。
std::move
の話に戻ると、上記の分類から std::move
はその引数を強制的に右辺値参照にキャストし、式全体としてはxvalueと認識されます。結果として右辺値と認識された結果、右辺値参照の引数にマッチするわけです。ややこしいですがそういうことです。
さて。ここから先に踏み込もうとすると、C++17とそれ以前の違いを強く意識することになります。C++17では左辺値と右辺値の定義が見直されただけでなく、初期化や戻り値の意味についても整理されています。この中ではRVOの位置づけなどもより明確になりますが、「ひとまずModernC++を読み書きできるようになる」目的からは大きく外れるように思われますので、本ガイドでは説明しないことにします。
(*)文字列リテラルだけは特別に左辺値扱いです。これは理解の妨げになりかねませんが、こういう特別な仕様があるということは頭の片隅においておいて損はないかも知れません。
さらに良いコードを書くために
正直、ここまでの文章を納得して理解できる方はModernC++をそれなりに読み書きできるようになっているのではと思います。また、「完全転送」とか「ユニバーサル参照」といった言葉をちらつかせ、混乱させつつマウントを取ってくる人には「私はアプリケーションプログラマだから関係ない!」と毅然とした態度を取れるようにもなったと思います。
が、しかし、必要に応じて言語仕様やライブラリ仕様を確認する必要が出ることも出てくるでしょう。また、テンプレートを書きたいとなった場合にはもっとツッコんだ理解が必要になってきます。手っ取り早く規格に基づいた解説を得たければ、C++ referenceを参照するというのが一つの選択肢です。
今後の動向
モジュール対応
C++のコンパイルはなぜ遅いのか?そりゃヘッダファイルを実装ファイルごとに処理してるからだよ、という問答が容易に成り立つわけです。ヘッダファイルなんて考え方はそろそろ卒業しましょう、モジュールというコンパイル単位で考えていきましょう、ということでC++20からモジュールの機能が導入されました。
import some_mod;
int main() {
some_lib::SomeObj s;
some_lib::some_func(s);
}
// 外部へエクスポートするモジュール名
export module some_mod;
// 名前空間とモジュール名は特に関係がない
namespace some_lib
{
// some_lib::SomeObjがモジュールsome_modとしてエクスポートされる
export class SomeObj
{
int x;
};
// some_lib::some_funcがモジュールsome_modとしてエクスポートされる
export void some_func(SomeObj& s)
{
// ...
}
};
このようなモジュール機構は他のプログラミング言語でも一般的にあるものですが、C++においてはいにしえからの #include
ディレクティブとの関係を強く意識した考え方になってはいます。言語機能的にはRust言語でのcrateの概念の影響を強く受けているようにも思われます。
標準ライブラリのモジュール化はC++23以降で検討されるとのことで、2021年時点ではヘッダファイルの #include
がなくなることはなさそうです。
コンセプト
イテレータへの対応では関連する begin
関数と end
関数を持っていることなどが条件として求められますが、こういった条件を言語仕様上明示的に取り扱う仕組みがありませんでした。こういった概念を明示的に取り扱うのがコンセプトです。
コンセプトは、テンプレート引数の型が満たすべき制約事項を明示します。
#include <functional> // 標準ライブラリのiteratorに似た感じのコンセプトを作る template <class T> concept Iter = requires (T& x) { // これだけのメンバ関数を持っていないといけないという制約 x.begin(); x.end(); x.operator++(); x.operator*(); }; // TをコンセプトIterにより制約する template <Iter T,typename ret,typename UnaryOperation> void iterate(T& x, UnaryOperation f) { for(auto i = x.begin(), e = x.end();i!=e;i++) { f(*i); } }
あっ、しかし、このガイドはテンプレート書く人のためのガイドではないのでした。ただ、これまでテンプレートを書く際にはテンプレートコード内のさまざまな型制約をミスマッチしてはバックトラックするという仕組みで動作しており(*)、型制約を表現する方法がありませんでした。コンセプトはこの問題を解決し、エラーメッセージを読みやすくする効果が期待されています。
まあ、ざっくりいうとRust言語のTraitみたいな機能ですね。(しかしTraitの方が概念として洗練されていると感じます。。。)
(*)この仕組みはSFINAE(Substitution failure is not an error)と呼ばれます。
さいごに
「だからなんなのか?」というと、、、多少の誤解はあるとはいえ、C++が重要であることには変わらないですね。だからこそ、このガイドをわざわざ読んでくれたのだと思いますし、ここで知った内容を活かしてC++の開発を進めていっていただければ幸いです。
ただ、C++の言語開発の動向を軽く追っていくと、言語の考え方がどんどんRust言語に近づいているなという印象を受けます。また、標準規格の策定の過程は実質的に大規模な社会実験となっており、その中で捨てられてゴミになってしまった仕様や仕組みが利用者を混乱させているというのも事実だと思います。
だから、あなたが今ModernC++を学んだ上で、より安全でスマートなプログラミングを目指したいというのであれば、C++と同様の目的を持ちながら、よりシンプルで洗練された仕組みを持つRust言語を学ぶというのも今後の一つの選択肢だと思います。ModernC++でできることの多くはRust言語でも実現できます。さらにいうと、ModernC++の概念の理解はRust言語を理解するための大きな助けになるでしょう。
つまり、このガイドを読んだことはきっと無駄にはならないだろう、ということですね。
(以上、おわりです)
# えっ、そんな結びでいいの?
あとがき的な
C++は「最低限のリテラシー」のしきい値が不明確なプログラミング言語で、人によって理解のレベルにかなりばらつきがあります。C++を読み書きするにあたって、最低限度のムーブセマンティクスに対する理解は必須だと思います。しかし、実際問題として、2021年時点で「C++が書けますか?」という質問に対して「書けます」と回答する人の多くはムーブセマンティクスを理解していないと思われます。
日々、我も彼もプログラミング言語以外の問題に忙殺される中で、こういった言語に関する基本的理解のギャップを埋めていくのはなかなかに大変です。「C言語が書けるならC++も書けるだろう」といった邪悪な推測があることも考えると、コードのメンテナンスを人にお願いする際などは不安でたまりません。
そして、いっぽうで、C++を学ぶ上では雑音も多いと感じます。そもそも言語機能が細かく、多く、難解です。それから、意図的かどうかは別として、いきなり本人の理解から遠く離れたキーワード(たとえば、「完全転送」とか、prvalue/xvalueが、とか)を投げつけて混乱させてしまっている例も多々見受けられます。
プログラミング言語の習得の鍵になるものはメンタルモデルの確立ではないか、ということをずっと以前から考えていました。構文からはじめて習熟しながら覚えていく、という外国語学習のようなやり方は効率が悪いのではないかと考えていました。このようなやり方自体を否定するわけではないのですが、少なくとも、既存のコーディング経験者が他の言語を覚えるときにはこの手順よりももっと近道があるのではないかと思っていました。
このガイド(の前半)の記載にあたっては、「C++のコーディングに最低限必要なメンタルモデル」に絞って短時間で読めるように配慮しながら書いたつもりです。この試行が実際にうまく、みんなをハッピーにできる、一つの解になれば良いなと考えています。
RISC-V原典 ざっと読みメモ
読もうと思っていたRISC-V原典を読む時間が少し取れたのでざっと読みしたメモを残しておく。
RISC-Vとは
RISC-Vは過去のプロセッサアーキテクチャに対する反省をくりこんだ新しいプロセッサの命令セットアーキテクチャである。
RISC-Vの開発はRISC-V基金で成り立っていて、著名なユーザー企業や既存プロセッサアーキテクチャからの脱却を目指すファブレスメーカーからの支援を受けている。
RISC-V原典
RISC-Vの命令セットアーキテクチャの要点をまとめた書籍。コアのアーキテクトである、David PattersonとAndrew Watermanの著書で原題は「The RISC-V Reader: An Open Architecture Atlas」。
タイトルに関して言うと正直、内容からするとこちらの原題のほうがしっくりくるが「Atlas」という言葉がそれほど一般的な用語でもないので編集者も悩んだのかな、などと。

- 作者: デイビッド・パターソン,アンドリュー・ウォーターマン,成田光彰
- 出版社/メーカー: 日経BP社
- 発売日: 2018/10/18
- メディア: 単行本
- この商品を含むブログを見る
内容についてのメモを少々
RV32I 基本整数ISA
- ソフト開発がしやすいようにレジスタの本数を32本用意する。
- 遅延スロットは規定しない。仕様が変わるとすぐに変動するから。
- 条件つき実行は使用しない。複雑さの根源になる。
- 3ソースオペランドも極力使用しない。
- PCは特別扱いする。いつでもジャンプする可能性があると構造が複雑になる。
- 構造が単純なことを持って実行効率を向上する。
まぁ、納得の行く話かなと。プログラマ観点で見るとARMでPC相対で飛ぶときに24とか16とか足してるのもなにそれってよく思っている。Thumbにジャンプするときに末尾のビット汚すやつとかもね。
RV32M 乗算および除算
ARMの整数除算が最近までなかった、という話をちょうど最近していたところだったのでちょっと印象に残っている。
あと、ARMだと32x32->64の乗算が単一の命令で実現できたような気がする。RISC-Vでは2命令に分かれるんだな。
RV32FおよびRV32D:単精度および倍精度の浮動小数点
- 浮動小数点レジスタは32本。
- 倍精度でも32本。
- 整数レジスタとの間の転送が可能。(過去アーキテクチャに対する反省点)
- 積和演算では3ソースオペランドが発生する。R4形式という。
- 符号インジェクションという命令があって便利。
RV32A アトミック命令
アトミック系の命令は下記の2パターンある。
- AMO アトミックメモリ操作
- ロードリザーブド/ストアコンディショナル
ロードリザーブド/ストアコンディショナルでまぁ、十分なはずだけど、プログラマ的にはアトミックメモリ操作が便利なので追加しましたってことらしい。
RV64 64ビットアドレス命令
- RV64I RV64M RV64A RV64F/D RV64C(!) に対応している。
- RV64VはRV32Vと同じ。
- RV128というのもギャグかと思いきや、大規模メモリ利用のために真面目に検討している。
RV32/64 特権アーキテクチャ
特権関連
- 特権に関わるモードはM/S/Uの三階層になっていてモダンなPOSIX向けのアーキテクチャである。(m:マシンモード、S:スーパーバイザーモード、U:ユーザーモード)
- 一応、wfiという割り込み待ちの命令がある。
- (当然だが)割り込みをSモードに委譲することができる。
- PMPというメモリ保護機能がある
ARMとかと同じでモードは三階層。PMPは使うのかなこれ、、、いらなかったのでは。
ページング関連
- 32ビットでは4K->4M->4Gという構造でページ構造を定義する。
- 64ビットでは39ビットモードと48ビットモードがある。ページテーブルが深くなる。
- TLBキャッシュクリアをハードウェアに伝える仕組みがある。
OpenModelicaを使って電気回路のシミュレーションをしてみる
マルチドメインモデリング環境である、OpenModelicaを使用した電気回路のシミュレーションの例を通して、その使用感や言語的側面に触れたいと思います。
想定読者としてはModelicaに触れたことがない人で、バックグラウンドとして回路シミュレーションに多少なりとも興味がある人、という感じでしょうか。ちょっとした使い方に関するチュートリアルも兼ねているかもしれません。
Modelicaとは
オブジェクト指向のモデリング言語です。他分野に跨る複雑なシステムのモデリング(例えば、機械、電気、電子、油圧、熱、制御、等々)のシミュレーションを記述するのが得意で、微分方程式なども記述できます。
ざっくりいうと、常微分方程式で解けるくらいの数値解析のためのモデリングをするのに便利な言語です。また、適用分野ごとに使える標準ライブラリなども用意されており、手早くモデルを構築することができます。いろいろな分野の計算に使えるので、「マルチドメインモデリング言語」と言われることもあります。
Modelica言語の仕様は Modelica Association で策定されており、最近ではModelica 3.4が2017年5月にリリースされました。
Modelica言語を実装するシミュレータとしては下記のようなものがあります。(Wikipediaさんありがとう)
- CATIA Systems
- Dymola
- LMS AMESim
- JModelica
- MapleSim
- MathModelica
- OpenModelica
- SCICOS
- SimulationX
メジャーなシミュレーションツールである、MATLABがこの中に入っていないところは注目すべき点かもしれません。
OpenModelicaとは
Open Source Modelica Consortium(OSMC)が開発を進めている、オープンソースのModelica処理系です。OSMC は Modelica Association と連携して規格を策定しているようです。
OSMCはこの他にFMI(Functional Mockup Interface)規格というのを策定していて、規格の異なるモデル間の協調動作を規定しています。相互運用性を高めてMATLABを食っちまおう!という意気込みを感じるような気もしますが、どうなんでしょうか。
さて、ソフトウェアとしてのOpenModelicaに話を戻して、OpenModelicaの構成等について説明します。
OpenModelicaに含まれるもの
OpenModelicaは概ね、下記のツールから成り立ちます。
- OMEdit - モデルエディタです。この後、これを使用してモデリングを実施します。
- OMPlot - プロッティングツールです。シミュレーション結果を表示します。
- OMShell - Modelica言語のインタープリタを起動します。いわゆるREPLです。
- OMNotebook - OpenModelicaのドキュメンテーションのために作られたツールのようです。立ち上げるとOpenModelicaのクイックツアーが出てきます。
まぁ、OMEditが本体かな、というところです。
早速モデルを作ってみる
まずは立ち上げる
OMEditを起動します。
左側のペインにパッケージがたくさん並んでいるのが見えます。
モデルを作る
とりあえず "New Modelica Class" とかやって、モデルを作ります。そして左側のペインからドラッグ&ドロップすると部品を落とし込めます。
部品のプロパティを設定できます。
出来上がったのがこちら。(3分クッキングかよ!)
昇圧チョッパー回路です。
感想としては、この手のツールのUIにしては使いやすい方です。
実行してみる。
とりあえず、実行。
とりあえず、何も考えずに一秒間、2000プロットくらいで試してみようと思います。
なにかエラーは出るが実行はされる。
適当に右側から項目を選ぶとプロットしてくれます。電圧を見てみます。
なんか、電圧跳ね上がってるし、落ち着いてないし、、、
電流も見てみます。
うーん、これはおそらくコンデンサのチャージ電流ですね。。。
(↑こいつのせいだ)
モデルをしげしげと眺めてみる
Modelica言語との関係
よく考えたら、Modelica言語全然出てきてない、って思います。(思うよね?)
ここまで表示してきたモデルは実はDiagram Viewというもので表示していました。これをText Viewに切り替えると。。。
Modelica言語による表現が出てきます。
なんとなく、HDL系の言語に雰囲気が似ていますかね。
これまで、ドロップしてきた部品のリストが上の方に出ています。実はペインの左側からドロップするという行為は、クラスのインスタンス化宣言をするということだったようです。また、部品のパラメータを入れるという行為はインスタンスの定数値を決定するということだったようです。
また、結線を行っていたものは下の方にconnect()という式によって表現されています。
なるほど。。。つまり、ここまで私はOMEditを使用してビジュアルプログラミングをしていたということになりそうです。
なお、annotation...となっている部分は表示上省略されていますが、これは図面の見え方を規定している模様です。
なんでこれで回路シミュレーションができるのか
さて、モデルを再度しげしげと見てみましょう。感想として「回路図やん!」というのが出てくると思います。(え?)
どう見ても回路図。。。
マルチドメイン言語じゃないの?回路だけ特別なの?って思ったりします。電流とか、電圧とか、それぞれ計算ルールが違うやん、それなのに、なんでつなぐだけで計算できるん??
これ、ちょっとだけ説明しておこうと思います。
実は、電気部品はそれぞれでPinというコネクタをインスタンス化しています。Pinの定義は下記のようになっています。
VoltageとCurrentを持っているということです。あれ?Currentにはflowというキーワードが、、、
Modelicaの世界では通常、接続された物同士は等しい物として扱われます。PinコネクタにおけるVoltageでいうと、接続した点での電圧が等しいという制約を受けることになります。つまりこれはキルヒホッフの電圧則を表しているということになりますね。一方、flowを付与されて接続されたもの同士は和がゼロになるという制約を受けることになります。これは、、、キルヒホッフの電流則ですね。
こうやって接続された物同士に異なる制約を設けることによって電気回路の法則を表現しているわけです。
まとめ(個人の感想です)
OpenModelicaの特徴として下記が挙げられます。
- その辺の簡単な回路シミュレーター位のことはやってのけてしまいます。
- マルチドメインで異なるドメインをまたいだシミュレーションもできます。
- ユーザーインタフェースは比較的使いやすいと思います。(圧倒的とは言わん)
- 自分でModelica言語を書いて拡張することもできます。
- オープンソースなので問題があったらフィードバックすることも、自分で修正することも可能です。
- なにより、購入する手間もなく、ダウンロードしてすぐに使えます。
これは、使ってみない手はないのではないでしょうか。
おわび
スナップショットの撮り方が怠惰だったらしく、ウィンドウの影が残ってしまいました。ちょっと暗い四角が写り込んでいるように見えるのはそのせいです。すみません。
MathJaxの試し書き2
下記のお題で書く。積分や分数を含む式。
Q.169 ☆5
— どちゃ楽数学bot (@so_easy_math) 2017年7月26日
次の極限値を計算せよ。
Find the limiting value.pic.twitter.com/T9mOJmpvJN
十分大きな については
において
であるから
より
感想
ここまで来たら何で書いても多分しんどい。数学やってる人はえらい。
MathJax記法の試し書き
はてながMathJaxに対応しているらしいので、試しに使用してみる。
題材は下記の問題の解答を書くこと、としてみる。
Q.127 ☆8 [高2鉄緑校内模試]
— どちゃ楽数学bot (@so_easy_math) 2017年7月25日
(a+b)(b+c)(c+a)=2^2016を満たす自然数の3つ組(a,b,c)は幾つあるか。
ここでは、 に対して下記の一般解を求めることから始める。
として一般性を損なわない。
右辺の素因数は2のべきなので、左辺の各項(?)は
と書くことができる。ここで の大小関係から
が言える。
明らかに いずれかに奇数があるとすると、他の2つも必ず奇数となる。逆に、いずれかに偶数があると、他の2つも必ず偶数となることが容易に言える。
上記から は常に偶数であり、この差を
と表すと、(a)の第一式から
と書ける。また、これを(a)の第二式、第三式に適用すると
となり、
となる。
(b)より、 となるためには
が必要。(a)とあわせると
となる。
ここで奇数解の場合、 となりうるのは
の時のみ。すなわち、
となり、解は
となる。
一方、偶数解においては に対する解
がある場合、
に対する解 が必ず存在することが容易に確認できる。この関係を偶数解である限り繰り返したどることで、
に対する偶数解
に対して、
に対する奇数解
を得ることができる。逆にいうと、偶数解はある
に対する奇数解
を用いて
に対する解
と表すことができる。
前記の議論から、すべての解は奇数解 を用いて
と表すことができる。
(c)における奇数解の定式化と(d)における一般化を踏まえれば を用いて
により におけるすべての解が得られることがわかる。なお、対応する
は
であり、すべての は異なる解を生成する。
原題はの解の数を求めるのであるから、(e)より
を満たす
の組み合わせの個数を数えれば良い。
これに該当するのは の計336個である。
感想
- 綺麗にかけるけど、MathJaxって表示遅いな、、、
- タグはすべての環境で有効なんだろうか。不明。
- 細かいことだがインラインの数式の前後にスペースを入れるのを忘れてはいけない。HTMLそのものが西洋言語向けの文化に基づいちゃってるので仕方ない部分はあるんだろうなとは思うけどめんどくさい。
- TeXをよく知らないからなんだろうけど、displaystyleを使うときに前後に改行を入れるかどうかで左寄せかセンター寄せかが変わる?そういうもんなのか?