Verbosity when optimizing with rvalue references

J

Juha Nieminen

Pete Becker said:
Yes. That's a very recent change, driven by the realization that going
the other way was dangerous.

So if a function takes n parameters, each one being either a regular
reference or an rvalue reference, you need to write 2^n overloaded
versions of the function to cover all cases?

Talk about code repetition...
 
S

Sousuke

Sousuke said:
I'm trying to create a Forwarded template class to do something
similar to perfect forwarding as presented by SG, but without having
to templatize the constructor. The ThreeStrings class should look like
this:
class ThreeStrings
{
public:
    ThreeStrings(
        Forwarded<std::string> s1,
        Forwarded<std::string> s2,
        Forwarded<std::string> s3
        ) : m_s1(s1), m_s2(s2), m_s3(s3)
    {
    }
private:
    std::string m_s1;
    std::string m_s2;
    std::string m_s3;
};
I've tried several permutations of the following:
template<class T>
class Forwarded
{
private:
    typedef decltype(std::forward<T>(*static_cast<T*>(nullptr)))
ForwardedType;
    ForwardedType m_forwarded;
public:
    template<class T2>
    Forwarded(T2&& forwarded) :
m_forwarded(std::forward<T>(forwarded))
    {
    }
    operator ForwardedType() const
    {
        return std::forward<T>(m_forwarded);
    }
};
However, when testing it with a class like ThreeStrings, it always
invokes the std::string copy constructor instead of the move
constructor, even when passing a std::string temporary.

Here's what I get under gcc-4.5.0 (I don't think I've picked up what
compiler you are using - VS2010, since you have nullptr?)

   13:30:52 Paul Bibbings@JIJOU
   /cygdrive/d/CPPProjects/CLCPP $cat forwarded_test.cpp
   // file: forwarded_test.cpp

   #include <iostream>
   #include <utility>

   #define nullptr 0

   template<class T>
   class Forwarded
   {
   private:
      typedef decltype(std::forward<T>(*static_cast<T*>(nullptr)))
         ForwardedType;
      ForwardedType m_forwarded;
   public:
      template<class T2>
      Forwarded(T2&& forwarded)
         : m_forwarded(std::forward<T>(forwarded))
      { }
      operator ForwardedType() const
      { return std::forward<T>(m_forwarded); }
   };

   struct B
   {
      explicit B(int)
      { std::cout << "B::B()\n"; }
      B(const B&)
      { std::cout << "B::B(const B&)\n"; }
      B(B&&)
      { std::cout << "B::B(B&&)\n"; }
   };

   class ThreeBs
   {
   public:
      ThreeBs(Forwarded<B> b1, Forwarded<B> b2, Forwarded<B> b3)
         : m_b1(b1), m_b2(b2), m_b3(b3)
      { }
   private:
      B m_b1;
      B m_b2;
      B m_b3;
   };

   int main()
   {
      B b1(1), b3(3);
      ThreeBs  threeBs(b1, B(2), b3);
   }

   13:30:58 Paul Bibbings@JIJOU
   /cygdrive/d/CPPProjects/CLCPP $i686-pc-cygwin-g++-4.5.0 -std=c++0x
     -O0 -static -o forwarded_test forwarded_test.cpp

   13:31:11 Paul Bibbings@JIJOU
   /cygdrive/d/CPPProjects/CLCPP $./forwarded_test
   B::B()
   B::B()
   B::B()
   B::B(B&&)
   B::B(B&&)
   B::B(B&&)

The copy constructor for B isn't used at all, evidently:

   13:34:35 Paul Bibbings@JIJOU
   /cygdrive/d/CPPProjects/CLCPP $nm --demangle --defined-only
      forwarded_test.exe | grep B\(
   0040c980 T B::B(B&&)
   0040c99c T B::B(int)

Also, a constructor for both the lvalue and rvalue reference arguments
are instantiated and ForwardedType evaluates to B&&.

   13:34:54 Paul Bibbings@JIJOU
   /cygdrive/d/CPPProjects/CLCPP $nm --demangle --defined-only
      forwarded_test.exe | grep Forwarded
   // ...
   0040c9b8 T ThreeBs::ThreeBs(Forwarded<B>, Forwarded<B>, Forwarded<B>)
   0040ca18 T Forwarded<B>::Forwarded<B&>(B&&&)
   0040ca30 T Forwarded<B>::Forwarded<B>(B&&)
   0040eed0 T Forwarded<B>::eek:perator B&&() const

This equates to the following (effective) instantiation of B:

   template<>
   class Forwarded<B>
   {
   private:
      B&& m_forwarded;
   public:
      template<>
      Forwarded<B&>(B& forwarded)
         : m_forwarded(std::forward<B>(forwarded))
      { }
      template<>
      Forwarded<B>(B&& forwarded)
         : m_forwarded(std::forward<B>(forwarded))
      { }
      operator B&&() const
      {
         return std::forward<B>(m_forwarded);
      }
   };

and, what is more, this /probably/ shouldn't work according to the
letter of the Standard, since for the lvalue reference constructor the
the result of std::forward<B>(forwarded) - an lvalue reference -
probably shouldn't bind to the rvalue reference m_forwarded [citation
needed :]

I'm getting a little lost in this, I have to say, but I wonder if your
problem (which appears to be slightly different from my problem here) is
that the type of Forwarded<T>::m_forwarded is dependent on T and is the
same for all objects of type Forwarded<T> for any T - T&&, in the case
of the above code compiled with gcc-4.5.0 - whereas the parameter of the
constructor may be either an lvalue or rvalue reference.  I think that
this *fixes* the type of `copy' that is applied - move, in my case;  you
say you're getting copy always.

Yes, that's the problem (or one of them...). For that class to work,
it would need to remember whether an lvalue or an rvalue was passed to
its constructor, and there's no way to do that statically. Something
like this would work:

template<class T>
class Forwarded
{
public:
Forwarded(const T& forwarded) :
m_forwarded(forwarded),
m_isLvalue(true)
{
}

Forwarded(T&& forwarded) :
m_forwarded(forwarded),
m_isLvalue(false)
{
}

bool IsLvalue() const { return m_isLvalue; }

const T& GetLvalue() const
{
return m_forwarded;
}

T&& GetRvalue() const
{
assert(!m_isLvalue);
return std::move(const_cast<T&>(m_forwarded));
}

private:
const T& m_forwarded;
bool m_isLvalue;
};

And ThreeStrings would look like this:

class ThreeStrings
{
public:
ThreeStrings(
Forwarded<std::string> s1,
Forwarded<std::string> s2,
Forwarded<std::string> s3
) :
m_s1(s1.IsLvalue() ? s1.GetLvalue() : s1.GetRvalue()),
m_s2(s2.IsLvalue() ? s2.GetLvalue() : s2.GetRvalue()),
m_s3(s3.IsLvalue() ? s3.GetLvalue() : s3.GetRvalue())
{
}

private:
...
};

The problem with this is that it introduces runtime constructs (as
opposed to std::forward), but that's the price you pay for not having
to templatize the constructor (or other methods).

(Actually there's a another problem: a temporary of the type of the
Forwarded template argument (std::string in this case) is being
created for no apparent reason, but that's a separate issue...)
Apologies if this is more confusing than helpful.

What's the relevance - or rather, what is the expected effect - of the
idiom:

   typedef decltype(std::forward<T>(*static_cast<T*>(nullptr)))
         ForwardedType;

I've not followed this through against the Standard, but the result of
static_cast<T*>(nullptr) is an rvalue, surely, but I can't guess what
that necessarily means for the result of *static_cast<T*>(nullptr).

I think what matters is not whether the function argument is an lvalue
or rvalue, but whether the template argument is an lvalue type or an
rvalue type. Since the template argument to Forwarded will rarely be a
template parameter (that's what we're trying to avoid in the first
place), and will instead be a hard-coded type (e.g., std::string),
then the above typedef will usually be an rvalue reference, regardless
of whether an lvalue or rvalue was passed to the Forwarded
constructor. That's the fundamental problem with my original Forwarded
class.
What is more, T cannot be a reference type - i.e., you couldn't form
ForwardedType<std::string&> because that would attempt to form a pointer
to a reference.  Same with ForwardedType<std::string&&>, I guess.

Yes, I guess that's yet another problem... :)
 

Ask a Question

Want to reply to this thread or ask your own question?

You'll need to choose a username for the site, which only take a couple of moments. After that, you can post your question and our members will help you out.

Ask a Question

Members online

Forum statistics

Threads
473,769
Messages
2,569,579
Members
45,053
Latest member
BrodieSola

Latest Threads

Top