勝手にコンストラクタを呼ばないで

はじめに

下記のコードを見てください.
このコードはクラスのインスタンス化を並列で行う目的で組んだものです.
見た感じでは正常に動きそうなのですが,実はコンパイルエラーが発生して動作することはありません.
そこで,本記事ではこの原因と解決策について考えて行こうと思います.
なお,実行環境はVC++19.15です.

#include <iostream>
#include <future>

class A {
	int m_n;
public:
	// constructor
	A() = delete;
	A(int n) :m_n(n) {}
	
	// method
	void print() const {
		std::cout << "value : " << m_n << std::endl;
	}
};

int main() {
	//並列でAのインスタンスを生成
	std::list<std::future<A>> threads;
	for (int j = 0; j < 3; ++j) {
		threads.emplace_back(std::async([j]() {
			return A(j);
		}));
	}

	for (auto&& t : threads) {
		t.get().print();
	}
}

コンパイルエラーの原因

まずはエラーの原因を考えます.コンパイル時の表示を見ると,
"'A::A(void)': 削除された関数を参照しようとしています"
という記述がありました.どうやら,引数なしコンストラクタあたりではじかれているようです.
そこで,クラスAのコンストラクタを以下のように書き替えてみました.

// constructor
A() :m_n(100) {
	std::cout << "normal constructor" << std::endl;
}
A(int n) :m_n(n) {
	std::cout << "constructor with argument" << std::endl;
}

出力

normal constructor
normal constructor
constructor with argument
normal constructor
constructor with argumentconstructor with argument
value : 0

value : 1
value : 2

並列処理をしているため出力がやや見づらいですが,なぜか引数なしコンストラクタが呼ばれています.
引数なしコンストラクタはどこでも呼んでいるつもりはなかったのですが,どうやらfutureが勝手に引数なしコンストラクタを呼んでいるようです.
そのため,引数なしコンストラクタをdeleteしていた元のコードでは実行できずコンパイルエラーが発生しました.

解決策

エラーの原因は分かったので,解決策を模索していきます.
私の知識では以下の3つの解決策を見出すことが出来ました.

  • 引数なしコンストラクタを定義する
  • threadを使って強引に解決する
  • futureのラップしたクラスを作る

以下の節でそれぞれ説明していきます.

引数なしコンストラクタを定義する

エラーの原因が"引数なしコンストラクタが定義されていない"ということであったので,それを定義してしまえば良いというのがこの解決策です.
コードも非常に簡単で,ただコンストラクタを定義する,もしくはデフォルト引数を指定するだけで終わりです.

// constructor
A() :m_n(10) {};
A(int n ) :m_n(n) {}
// constructor
A(int n = 10) :m_n(n) {}

一見単純で良さそうに見えるこの方法ですが,実は致命的な問題点があります.
それは,引数なしコンストラクタが定義されてしまう,という点です.
通常,綺麗な設計であればオブジェクトは不変であり,生成した後から値を変更することが必要となる引数なしコンストラクタはあるべきではありません.
そのため,引数なしコンストラクタを定義するこの解決策は設計をぐちゃぐちゃにすることと同義であり,やってはいけない方法と言えます.
※2019/08/07 追記 よくよく考えるとデフォルト引数を指定する方法で設計的にも問題はなさそうです.
現状ではこの方法が良いでしょう.

threadを使って強引に解決する

この方法はクラスAの設計を変えることなく半ば強引に解決する方法です.
futureを使っていたことが問題の発端であったため,threadを使います.futureと違い同期時に値を返すということはできませんが,並列処理中にインスタンスをリストに格納していくことでなんとなくそれっぽい動作が実現できます.
しかし,これはemplace_backがスレッドセーフであることを前提としています.
私の調査不足ではありますが,調べていてもemplace_backがスレッドセーフであることが断定できなかったため,この手法が正常に動く保証ができません.

int main(){
	std::list<A> instanceList;

	//並列でAのインスタンスを生成してリストに格納する
	std::list<std::thread> threads;
	for (int j = 0; j < 3; ++j) {
		threads.emplace_back(std::thread([j,&instanceList]() {
			instanceList.emplace_back(j);
		}));
	}

	for (auto&& t : threads) {
		t.join();
	}

	for (const auto& instance : instanceList) {
		instance.print();
	}
}

futureのラップしたクラスを作る

この方法は,futureがだめなら新しいfutureを作ってしまおう,という思想です.
新しく設計したFutureクラスの内部でダミークラスを生成し,真のクラスの引数なしコンストラクタを呼ぶ代わりにダミークラスの引数なしコンストラクタを呼ばせるようにします.
しかしよくよく考えてみると,結局引数なしコンストラクタをダミーとは言え定義することとなっています.あれれ~おっかしいぞ~.

終わりに

なんだかんだでうまい解決法は思い浮かびませんでした.
当面はthreadを使っていく予定ですが,挙動が安定するのかとても不安です.
C++の並列処理に詳しい方がいらっしゃいましたらぜひご教授いただきたいです.