ことの発端
昨日の「template パラメータ内でラムダ式」という記事で decltype
が消化できていないので、テストコードを書いて腹落ちさせたい。
更に、std::unique_ptr
に副作用がある変数をキャプチャさせた際にどうなるかとコードを書いてみたら、コンパイラが激おこだったので、理由を知りたいという動機です。
# include <iostream>
# include <memory>
int main(void)
{
const auto deleter1 = [](char* p){ ::free(p); };
std::unique_ptr<char, decltype(deleter1)> p1(reinterpret_cast<char*>(malloc(1)));
bool bDelete = false;
const auto deleter2 = [&bDelete](char* p){ if(bDelete) ::free(p); };
std::unique_ptr<char, decltype(deleter2)> p2(reinterpret_cast<char*>(malloc(1)));
}
Main.cpp:10:45: error: no matching constructor for initialization of 'std::unique_ptr<char, decltype(deleter2)>' (aka 'unique_ptr<char, const (lambda at Main.cpp:9:25)>')
std::unique_ptr<char, decltype(deleter2)> p2(reinterpret_cast<char*>(malloc(1)));
^ ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
テストコード1
いきなり難しい事ができないので、まずは簡単なものからやってみる。
decltype(expr)
がexprの型を返すのは知っているが、改めてそれを実感してみる。
# include <iostream>
int main(void)
{
std::string s = "ABC";
using stype = decltype(s);
stype ss = "DEF";
std::cout << typeid(s).name() << std::endl;
std::cout << typeid(s).name() << std::endl;
}
NSt7__cxx1112basic_stringIcSt11char_traitsIcESaIcEEE
NSt7__cxx1112basic_stringIcSt11char_traitsIcESaIcEEE
ここまでは良いとして。
テストコード2
次に本題のラムダ式で decltype
を使ってみる。
# include <iostream>
int main(void)
{
const auto f = [](int a, int b){
return a + b;
};
using ftype = decltype(f);
ftype ff;
std::cout << typeid(f).name() << std::endl;
std::cout << typeid(ff).name() << std::endl;
std::cout << typeid(decltype(f(0,0))).name() << std::endl;
std::cout << typeid(decltype(ff(0,0))).name() << std::endl;
}
Z4mainE3$_0
Z4mainE3$_0
i
i
マングリング化されていて分かりにくいが、ラムダ式によって $_0
という型が定義1され、続けて $_0
をインスタンス生成し、そいつが f に代入されると。
ちと、f(0,0)
とか f(0,0)
が int
という型になるのは delctype
仕様で理解はしているが、関数型が取得できても良いような気がするが、それは仕様の問題ないし別に腹落ちしない訳じゃないので良しとする。(上から目線でごめんなさい)
テストコード3
で、コンパイラが激おこだったコードに近いのかな?
副作用を持つ関数オブジェクトの場合です。
# include <iostream>
int main(void)
{
auto bAdd = false;
const auto f = [&](int a, int b){
return bAdd ? a + b : a - b;
};
using ftype = decltype(f);
// ftype ff; /* error: no matching constructor for initialization of 'ftype' */
std::cout << typeid(f).name() << std::endl;
// std::cout << typeid(ff).name() << std::endl;
std::cout << typeid(decltype(f(0,0))).name() << std::endl;
}
Z4mainE3$_0
i
ftype ff;
でエラーになります。
そりゃそうです。
キャプチャした変数はクラスオブジェクトのメンバ変数になる訳で、そいつを初期化して生成しないとインスタンス生成なんてできる訳ない。
そして、初期化する術を用意していない。2
コンパイラのエラーは理解したが、このエラーが出るって事は、誰かが生成をしようとしているに違いない。
もう、型と値(オブジェクトインスタンス)が混在して迷子になることが多い。。。
で、「ことの発端」である std::unique_ptr
のテンプレートパラメータ仮引数が値ではなく型だということを改めて理解し、decltype
を使う理由はここで腹落ちしました。
テストコード4
でも、もう少しやってみます。
次は、ラムダ式の生成と破棄のタイミングを知りたくて3、ラムダ式っぽいことを自作してみます。
(テストコード4では関数オブジェクト型定義まで)
# include <iostream>
# include <memory>
int main(void)
{
struct deleter_t {
deleter_t() { std::cout << "deleter_t::ctor()" << std::endl; }
~deleter_t() { std::cout << "deleter_t::dtor()" << std::endl; }
void operator () (char* p) const { std::cout << "deleter_t::operator()" << std::endl; ::free(p); }
};
std::cout << "scope in" << std::endl;
{
std::unique_ptr<char, deleter_t> p(reinterpret_cast<char*>(malloc(1)));
}
std::cout << "scope out" << std::endl;
}
scope in
deleter_t::ctor()
deleter_t::operator()
deleter_t::dtor()
scope out
ここまでくると、ふむふむって感じで、ようやく気持ちが穏やかな世界に戻れました。
余談ですが、operator ()
がconst
をいつもの癖で付加していたけど、ラムダ式でこの const
属性を外すのが mutable
という仕様も理解しました。4
テストコード5
「テストコード4」の続きで decltype
が使われて機能している感じを知りたく、さらにラムダ式に寄せるべく、クラスオブジェクトの生成を行います。
# include <iostream>
# include <memory>
int main(void)
{
struct deleter_t {
deleter_t() { std::cout << "deleter_t::ctor()" << std::endl; }
~deleter_t() { std::cout << "deleter_t::dtor()" << std::endl; }
void operator () (char* p) const { std::cout << "deleter_t::operator()" << std::endl; ::free(p); }
};
const auto deleter = deleter_t();
/* 下記のラムダ式は概ね上記の「型定義」と「オブジェクト生成」をまとめた糖衣構文 */
// const auto deleter = [](char* p){ ::free(p); };
std::cout << "scope in" << std::endl;
{
std::unique_ptr<char, decltype(deleter)> p(reinterpret_cast<char*>(malloc(1)));
}
std::cout << "scope out" << std::endl;
}
deleter_t::ctor()
scope in
deleter_t::ctor()
deleter_t::operator()
deleter_t::dtor()
scope out
deleter_t::dtor()
ここにきて、そうか、ラムダ式で得られたオブジェクトをdecltype
するんだから、無駄な生成と破棄が多く発生するのか。。。
いや、もう凡人はここまでで良いでしょう。
ようやく、これで、ゆっくり眠れそう。
ふと
using deleter_t = decltype([](char* p){::free(p);});
std::unique_ptr<char, decltype([](char* p){::free(p);})> p(reinterpret_cast<char*>(malloc(1)));
こんなこと考えて、「template パラメータ内でラムダ式」にループしそう。。。