Best Practice: Bedingte Kopie



  • Hallo

    Was bei mir immer mal wieder auftritt, ist, dass ich bedingt eine teure Kopie machen muss innerhalb einer Funktion. Das soll nicht nötig sein, wenn die Funktion mit einer Rvalue-Referenz aufgerufen wird. Im folgenden Beispiel wird die Funktion g das Argument verändern:

    template< typename T >
    int g( T& x );
    
    template< typename T >
    int f( T&& x ) {
        if( x.size() == 42 ) // irgendeine Bedingung, die das Kopieren hinfällig macht
            return 0;
        auto Copy = std::forward< T >( x );
        return g( Copy );
    }
    

    Meine Frage: Würdet ihr das gleich machen? Oder gibt es hierfür eine einfachere Lösung, die ich gerade übersehe?

    LG



  • Einfacher vermutlich nicht.
    Aber es gibt sicher ne kompliziertere Möglichkeit, die den Move vermeidet wenn das Argument von f schon ne Rvalue-Referenz ist 😉


  • Mod

    hustbaer schrieb:

    Aber es gibt sicher ne kompliziertere Möglichkeit, die den Move vermeidet wenn das Argument von f schon ne Rvalue-Referenz ist 😉

    Blöd ist, dass die Anzahl der Objekte, die beim verlassen des Scopes zerstört werden müssten, unterschiedlich wäre, das geht nicht so ohne Weiteres.
    Möglich wäre etwas in der Art:

    template< typename T >
    int g( T& x );
    
    template< typename T >
    int f( T&& x ) {
        if( x.size() == 42 ) // irgendeine Bedingung, die das Kopieren hinfällig macht
            return 0;
        return []( auto&& copy ) {
            // mach was mit copy
            return g( copy );
        }( std::is_lvalue_reference< T >{} ? forward< std::remove_reference_t< T > >( T{ x } ) : forward< T >( x ) );
    }
    


  • Hätte jetzt eher an sowas gedacht...

    #include <type_traits>
    
    namespace detail {
    
    	template <class T>
    	class forward_ref_t {
    	public:
    		template <class U>
    		forward_ref_t(U&& u) : m_ptr(&static_cast<U&>(u)) { }
    
    		T& ref() { return *m_ptr; }
    
    	private:
    		T* m_ptr;
    	};
    
    	template <class T>
    	class forward_copy_t {
    	public:
    		template <class U>
    		forward_copy_t(U&& u) : m_copy(std::forward<U>(u)) { }
    
    		T& ref() { return m_copy; }
    
    	private:
    		T m_copy;
    	};
    
    	template <class T>
    	struct can_forward_as_ref
    	{
    		static bool const value = std::is_rvalue_reference<T>::value && !std::is_const<std::remove_reference<T>::type>::value;
    	};
    
    	template <class T>
    	struct base_type : std::remove_cv<typename std::remove_reference<T>::type> { };
    
    	template <class T, bool as_ref>
    	struct forward_buffer_type_impl;
    
    	template <class T>
    	struct forward_buffer_type_impl<T, true> {
    		typedef typename forward_ref_t<typename base_type<T>::type> type;
    	};
    
    	template <class T>
    	struct forward_buffer_type_impl<T, false> {
    		typedef typename forward_copy_t<typename base_type<T>::type> type;
    	};
    
    	template <class T>
    	struct forward_buffer_type : forward_buffer_type_impl<T, can_forward_as_ref<T>::value> { };
    
    } // namespace detail
    
    template <class T>
    typename detail::forward_buffer_type<T&&>::type forward_or_copy(typename std::remove_reference<T>::type&& t) {
    	return std::forward<T>(t);
    }
    
    template <class T>
    typename detail::forward_buffer_type<T&&>::type forward_or_copy(typename std::remove_reference<T>::type& t) {
    	return std::forward<T>(t);
    }
    
    template< typename T >
    int g(T& x) {
    	x.mutate();
    	return x.size();
    }
    
    template< typename T >
    int f(T&& x) {
    	if (x.size() == 42) // irgendeine Bedingung, die das Kopieren hinfällig macht
    		return 0;
    	return g(forward_or_copy<T>(x).ref());
    }
    
    ////////////////////////////////////////////////////////////////////////
    
    #include <iostream>
    
    class Movable {
    public:
    	explicit Movable(size_t size) : m_size(size) {
    		std::cout << "construct " << m_size << "\n";
    	}
    
    	Movable(Movable const& other) : m_size(other.m_size) {
    		std::cout << "copy-construct " << m_size << "\n";
    	}
    
    	Movable(Movable&& victim) : m_size(victim.m_size) {
    		victim.m_size = 0;
    		std::cout << "move-construct " << m_size << "\n";
    	}
    
    	~Movable() {
    		if (m_size)
    			std::cout << "destruct " << m_size << "\n";
    		else
    			std::cout << "destruct zombie\n";
    	}
    
    	Movable& operator =(Movable&& victim) {
    		m_size = victim.m_size;
    		victim.m_size = 0;
    		std::cout << "move-assign " << m_size << "\n";
    	}
    
    	size_t size() const { return m_size; }
    	void mutate() { m_size++; }
    
    private:
    	size_t m_size;
    };
    
    int main()
    {
    	std::cout << "calling with lvalue...\n";
    	{
    		Movable m(10);
    		auto const result = f(m);
    		std::cout << "result: " << result << "\n";
    	}
    
    	std::cout << "calling with rvalue...\n";
    	{
    		auto const result = f(Movable(20));
    		std::cout << "result: " << result << "\n";
    	}
    
    	return 0;
    }
    

    Geht aber sicher noch schöner (hab' mit C++11 Features keine Übung). Und zahlt sich natürlich nur aus wenns an mehr als nur 1-2 Stellen gebraucht wird.

    ps: Fehlt bei dir ein _v (bei is_lvalue_reference< T >{} )?


  • Mod

    hustbaer schrieb:

    ps: Fehlt bei dir ein _v (bei is_lvalue_reference< T >{} )?

    std::integral_constant<T,..> hat einen Konvertierungsoperator nach T. Aus diesem Grunde sind die *_v Aliase nicht so nützlich wie *_t
    Zudem gibt es sie erst ab C++17.

    Es geht auch noch ein bisschen kürzer:

    #include <type_traits>
    #include <utility>
    template <class T>
    std::conditional_t<std::is_reference<T>{} || std::is_const<T>{}, std::remove_cv_t<std::remove_reference_t<T>>, T&&> forward_or_copy(T& t) {
        return std::forward<T>(t);
    }
    template <typename T>
    int g(T& x) {
        x.mutate();
        return x.size();
    }
    template <typename T>
    int f(T&& x) {
        if (x.size() == 42) // irgendeine Bedingung, die das Kopieren hinfällig macht
            return 0;
        auto&& xx = forward_or_copy<T>(x);
        return g(xx);
    }
    ////////////////////////////////////////////////////////////////////////
    #include <iostream>
    class Movable
    {
    public:
        explicit Movable(size_t size) : m_size(size) { std::cout << "construct " << m_size << "\n"; }
        Movable(Movable const& other) : m_size(other.m_size) { std::cout << "copy-construct " << m_size << "\n"; }
        Movable(Movable&& victim) : m_size(victim.m_size) {
            victim.m_size = 0;
            std::cout << "move-construct " << m_size << "\n";
        }
        ~Movable() {
            if (m_size)
                std::cout << "destruct " << m_size << "\n";
            else
                std::cout << "destruct zombie\n";
        }
        Movable& operator=(Movable&& victim) {
            m_size = victim.m_size;
            victim.m_size = 0;
            std::cout << "move-assign " << m_size << "\n";
            return *this;
        }
        size_t size() const { return m_size; }
        void mutate() { m_size++; }
    private:
        size_t m_size;
    };
    int main() {
        std::cout << "calling with lvalue...\n";
        {
            Movable m(10);
            auto const result = f(m);
            std::cout << "result: " << result << "\n";
        }
        std::cout << "calling with rvalue...\n";
        {
            auto const result = f(Movable(20));
            std::cout << "result: " << result << "\n";
        }
    }
    


  • Ein bisschen, ja 😃 Und ist auch viel schöner. 👍

    D.h. man kann die Lebenszeit eines Temporaries verlängern indem man ne Rvalue-Referenz damit initialisiert - so wie es auch mit const Lvalue-Referenzen geht? Würde ja auch irgendwie Sinn machen...

    Potentiell verwirrend bei der Lösung ist natürlich dass man es jetzt nicht mehr ohne die Hilfsvariable schreiben kann. Ist aber IMO trotzdem viel schöner.


  • Mod

    hustbaer schrieb:

    Potentiell verwirrend bei der Lösung ist natürlich dass man es jetzt nicht mehr ohne die Hilfsvariable schreiben kann.

    Man könnte so etwas wie

    template <typename T>
    T& as_lvalue(T&& v) {
        return v;
    }
    
    template <typename T>
    int f(T&& x) {
        if (x.size() == 42) // irgendeine Bedingung, die das Kopieren hinfällig macht
            return 0;
        return g(as_lvalue(forward_or_copy<T>(x)));
    }
    

    machen.


Anmelden zum Antworten