スポンサーリンク

コードが書ける!数式が書ける!AAが書ける!スタンプが貼れる!

無料の匿名掲示板型SNS「このはちゃんねる

新規会員募集中!

続々:可変長引数を受け取って、警告を出力するだけの関数

274, 2019-12-29

目次

値渡しだこれ

前々回前回の続き。可変長実引数を渡せるようになったのはいいが、今のままだと全部値渡しになっている。メンバにoperator<< を持つCatクラスを作って warn()に渡して確認してみる。

#include <iostream>
#include <string>

namespace Debug {
// warn内で使う関数群。
namespace Secret {
template<class T = void>
void impl()
{
    // nothing.
}

template<class T, class... Ts>
void impl(T value, Ts... args)
{
    impl(args...);
}
}// namespace Secret

/**
 * 標準エラーに出力。
*/
template<class... Ts>
void warn(Ts... args)
{
    Secret::impl(args...);
}
}// namespace Debug

struct Cat
{
    Cat()
    {
        std::cout << "Cat: constructor.\n";
    }

    Cat(Cat const& other)
    {
        std::cout << "Cat: copy constructor.\n";
    }

    Cat(Cat&& other)
    {
        std::cout << "Cat: move constructor.\n";
    }

    friend std::ostream& operator<<(std::ostream& o, Cat const& other)
    {
        o << "nyaa";
        return o;
    }
};

int main()
{
    std::string hello = "hello!";
    Cat cat;

    Debug::warn(hello, " cat ", "says ", cat);

    return 0;
}
$ g++ -std=c++11 -Wall test.cpp && ./a.out 
Cat: constructor.
Cat: copy constructor.
Cat: copy constructor.
Cat: copy constructor.
Cat: copy constructor.
Cat: copy constructor.

warn()内で再帰的にParameter packを展開するので、実引数の数だけCatのコピーコンストラクタが呼ばれていた(アカン)。

lvalue reference / rvalue referenceで何とかする

参考:
本の虫 - rvalue reference 完全解説
本の虫 - rvalue reference 補足
ここは匣 - const rvalue referenceは何に使えばいいのか
ロベール - リテラル文字

参考によると、template関数の引数におけるrvalue referenceは、ほかとは少し違うという。


template < typename T >
void f( T && t ) {}

中略
f()に、lvalue referenceを渡すと、TがX &になり、続く&&は無視され、lvalue referenceとして取り扱われる。

テンプレート関数fから別の関数に引数を渡したい場合は

std::move()は、lvalueをmoveしたいときに使い、std::forward()は、テンプレート関数の引数を、そのまま別の関数に渡したい時に使う。

から、std::forward()を使う。参考によるとstd::forward()はargument deduction(テンプレート引数の型を推定してくれる機能)に関係するので、テンプレート関数ではstd::forward(), ほかではstd::move()を使う。
 std::forward()は賢い子で、lvalue referenceを渡されたらlvalue referenceを、rvalue referenceを渡されたらrvalue referenceを返してくれる。

template<class T>
void f(T&& value)// ここではどっちかわからないが、
{
    std::forward<T>(value);// 判別してくれる。
}

これで材料が揃ったので、lvalue reference / rvalue referenceを使ってParameter packを展開してみる。

#include <iostream>
#include <typeinfo>
#include <cxxabi.h>

/**
 * ref: http://cpplover.blogspot.jp/2010/03/gcc.html
*/
class Demangle
{
private:
    char* realname;

public:
    Demangle(std::type_info const& ti)
    {
        int status = 0;
        realname = abi::__cxa_demangle(ti.name(), 0, 0, &status);
    }

    Demangle(Demangle const &) = delete;
    Demangle& operator=(Demangle const&) = delete;

    ~Demangle()
    {
        std::free(realname);
    }

    operator char const* () const
    {
        return realname;
    }
};

template<class T = void>
void impl()
{}

template<class T, class... Ts>
void impl(T&& value, Ts&&... args)
{
    // ref: http://stackoverflow.com/questions/16748539/typeid-of-template-parameter-universal-reference
    std::cout << "'" << value << "(" << Demangle(typeid(T)) << ")' is a "
        << (std::is_const<typename std::remove_reference<T>::type>::value ? "const " : "")
        << (std::is_lvalue_reference<T>::value ? "lvalue" : "rvalue" ) << " reference"
        << std::endl;
    impl(std::forward<Ts>(args)...);
}

template<class... Ts>
void f(Ts&&... args)
{
    impl(std::forward<Ts>(args)...);
    std::cout << '\n';
}

int main()
{
    f(1, '2', 3.f, "4", 5.0);

    f("cat", std::move("dog"));

    std::string cat("cat");
    std::string dog("dog");
    f(cat, std::move(dog), std::string("bird"));
    return 0;
}
}
$ g++ -std=c++11 -Wall test2.cpp && ./a.out
'1(int)' is a rvalue reference
'2(char)' is a rvalue reference
'3(float)' is a rvalue reference
'4(char [2])' is a const lvalue reference
'5(double)' is a rvalue reference

'cat(char [4])' is a const lvalue reference
'dog(char [4])' is a const rvalue reference

'cat(std::string)' is a lvalue reference
'dog(std::string)' is a rvalue reference
'bird(std::string)' is a rvalue reference

結果を見ると、マジックナンバーはrvalue reference、リテラル文字列"4"はconstなlvalue referenceで渡されている。参考によると、リテラル文字列はメモリ上の静的な領域に置かれる静的なデータで、その寿命はプログラムが死ぬまで。そう考えると納得がいく。さらにこちらの記事を見つけた。

...加えて、const rvalueはあまり意味がない。上記のg()のようにconst rvalueを返す関数を定義したり、std::move()をconstオブジェクトに対して呼ぶ事も出来るが、特に意味は無い。どうして、関数を呼び出す側が関数の戻り値のコピー対して出来る事に制限を課すんだ?どうして、変更不可能なオブジェクトをムーブしようとするんだ?

どうして、変更不可能なオブジェクトをムーブしようとするんだ?

はわわ・・・。

lvalue reference / rvalue reference で書きなおす

lvalue reference, rvalue referenceで書き直し。

インターフェース

#ifndef INCLUDED_DEBUG_H
#define INCLUDED_DEBUG_H

#include "details_debug.hpp"

namespace Debug {

template<class... Ts> void halt(Ts&&... args);
template<class... Ts> void warn(Ts&&... args);

}// namespace Debug
#endif/*INCLUDED_DEBUG_H*/

実装

#ifndef INCLUDED_DETAILS_DEBUG_H
#define INCLUDED_DETAILS_DEBUG_H
#include <iostream>
#include <cstring>

namespace Debug {

// warn、halt内で使う関数群。
namespace Secret {
template<class T = void>
void impl()
{
    // nothing.
}

template<class T, class... Ts>
void impl(T&& value, Ts&&... args)
{
    std::cerr << value;
    impl(std::forward<Ts>(args)...);
}
}// namespace Secret

/**
 * 標準エラーに出力。
*/
template<class... Ts>
void warn(Ts&&... args)
{
    std::cout << std::flush;
    std::cerr << "W: ";
    Secret::impl(std::forward<Ts>(args)...);
    std::cerr << ": " << std::strerror(errno) << '.' << std::endl;
}

/**
 * 標準エラーに出力して死ぬ。
*/
template<class... Ts>
void halt(Ts&&... args)
{
    std::cout << std::flush;
    std::cerr << "E: ";
    Secret::impl(std::forward<Ts>(args)...);
    std::cerr << ": " << std::strerror(errno) << '.' << std::endl;
    quick_exit(EXIT_FAILURE);
}

}// namespace Debug
#endif/*INCLUDED_DETAILS_DEBUG_H*/

テスト内容

#include "debug.hpp"
#include <iostream>
#include <string>

struct Cat
{
    ~Cat()
    {
        std::cout << "Cat: destructor.\n";
        if (mWord)
            delete mWord;
    }

    Cat(char const* word)
        :mWord(new std::string(word))
    {
        std::cout << "Cat: constructor.\n";
    }

    Cat(Cat const& other)
        :mWord(new std::string(*other.mWord))
    {
        std::cout << "Cat: copy constructor.\n";
    }

    Cat(Cat&& other)
    {
        std::cout << "Cat: move constructor.\n";
        mWord = other.mWord;
        other.mWord = nullptr;
    }

    friend std::ostream& operator<<(std::ostream& o, Cat const& other)
    {
        if (other.mWord)
            o << *(other.mWord);
        return o;
    }

    void cout_word_pointer_address() const
    {
        std::cout << "Cat mWord address: " << (void*)mWord << '\n';
    }

private:
    std::string* mWord;
};

int main()
{
    using namespace Debug;
    std::string hello = "hello!";
    Cat cat("nyaa");

    warn(hello, " cat ", "says '", cat, "'");
    halt(hello, " cat ", "says '", cat, "' and good bye.");

    return 0;
}
$ g++ -std=c++11 -Wall main.cpp && ./a.out 
Cat: constructor.
W: hello! cat says 'nyaa': Success.
E: hello! cat says 'nyaa' and good bye.: Success.

main()内のcatは、warn(), halt()にlvalue referenceで渡しているので、コピーコンストラクタが呼ばれなくなった。リテラル文字列もさっきの例からconst lvalue referenceで渡されているはず。

そうだ、moveしてみよう

「details_debug.hpp」にテンプレート関数dust()を追加して、impl()とwarn()に手を加えた。

namespace Secret {

template<class T>
void dust(T value)
{}

...
template<class T, class... Ts>
void impl(T&& value, Ts&&... args)
{
    // valueの型がrvalue referenceであればdust()にmoveするはず。
    dust(std::forward<T>(value));
    impl(std::forward<Ts>(args)...);
}
}// namespace Secret

template<class... Ts>
void warn(Ts&&... args)
{
    Secret::impl(std::forward<Ts>(args)...);
}

「main.cpp」のmain関数を次のように変更。helloとcatをstd::move()でキャストして渡す。

int main()
{
    using namespace Debug;

    std::string hello = "hello!";
    Cat cat("nyaa");

    std::cout << "hello: " << hello << std::endl;
    cat.cout_word_pointer_address();

    warn(std::move(hello), " cat ", "says '", std::move(cat), "'");

    std::cout << "hello: " << hello << std::endl;
    cat.cout_word_pointer_address();

    return 0;
}
$ g++ -std=c++11 -Wall test.cpp && ./a.out 
Cat: constructor.
hello: hello!
Cat mWord address: 0x92bd020
Cat: move constructor.
Cat: destructor.
hello: 
Cat mWord address: 0
Cat: destructor.

helloは空文字列、catはmove constructorが働いてメンバ変数mWordのポインタが移動した。


結論

warn(), halt()にはlvalue referenceやrvalue referenceの両方を渡せないと困るので、しばらくはこの設計で使ってみようと思います。
slide share - constexpr idiomsを見ると、再帰深度の問題もあるようです。

投稿者名です。64字以内で入力してください。

必要な場合はEメールアドレスを入力してください(全体に公開されます)。

投稿する内容です。

スポンサーリンク

スポンサーリンク

スポンサーリンク

スポンサーリンク