3
\$\begingroup\$

In our software we have some functions that return information through lvalue reference parameters, which, in some cases, are irrelevant for the calling code. For that reason, I tried to come up with an object that can be used instead of a previously declared dummy variable, similar to std::ignore with std::tie (cannot fully take advantage of C++17 yet):

namespace detail {
    template<typename T>
    struct ignorer_impl {
        static_assert(std::is_trivially_copyable_v<T> || std::is_default_constructible_v<T>,
            "can only be used on trivially copyable types or default constructible types");

        alignas(alignof(T)) unsigned char m_buf[sizeof(T)];

        constexpr operator T& () noexcept {
            return *reinterpret_cast<T*>(m_buf);
        }
    };
}

template<typename T, typename = void>
struct ignorer : detail::ignorer_impl<T> {};

template<typename T>
struct ignorer<T, std::enable_if_t<std::is_trivially_copyable_v<T>>> : detail::ignorer_impl<T> {};

template<typename T>
struct ignorer<T, std::enable_if_t<!std::is_trivially_copyable_v<T> && std::is_default_constructible_v<T>>> : detail::ignorer_impl<T> {
    constexpr ignorer() noexcept(std::is_nothrow_default_constructible_v<T>) {
        new (detail::ignorer_impl<T>::m_buf) T{};
    }
};

This can then be used like so:

struct X {
    double f;
    int i;
    std::string s;
};

void foo(X& x) {
    x.s = "Hello, world!";
}

int main() {
    foo(ignorer<X>{});

    return 0;
}

Does anybody have suggestions or see anything that's totally bad?

Also, can you come up with a way that would not require me to explicitly provide the type when using the object (using any C++ version)? (I don't think there's a way, but I'll ask anyway :D)

\$\endgroup\$
1
  • \$\begingroup\$ Welcome to the Code Review Community. We only review real working code from your project, and the usage examples appear to be theoretical in nature which is off-topic. Please read our help pages starting with [How do I ask a good question?] (codereview.stackexchange.com/help/how-to-ask). \$\endgroup\$
    – pacmaninbw
    Sep 22 at 14:34
7
\$\begingroup\$

What's wrong with just writing:

int main()
{
    X ignore;
    foo(ignore);

    return 0;
}

What benefit is gained by adding a wrapper around the unused argument?


As for your wrapper presented, what does it do? It appears to be an elaborate way to put a T inside another struct, except it prevents calling the destructor. I wonder why you would want to do this, and furthermore observe that in your own example this causes a memory leak as the std::string is not freed.

Your enable_if cases prevent it from calling the constructor if it's trivially copyable. I'm not sure that's the right case for noting that the default constructor doesn't do anything interesting (that's not copying it), and for types like primitive int skipping the constructor is contrary to your use of {} in the invocation.


Update

After seeing your comment

I was just looking for a "cleaner" way to write it without having to the declare the variable beforehand.

I'd like to make a couple of observations.

simplify your solution

The reason why you can't write foo(X{}) is because there is a specific rule that prevents binding a non-const rvalue to an lvalue reference. This is to prevent accidents where a temporary, perhaps from an automatic conversion, is passed which makes it not function as an "out" parameter.

The operational part of your solution is the operator T&(). Nothing about your buffer and placement new has any bearing on it! Having a base class with different cases... that's getting off in the weeds.

Here is my improved version of your idea:

template<typename T>
class ignorer {
    T dummy;
public:
    constexpr operator T& () noexcept
       {  return dummy;  }
};

I'm guessing your work to avoid construction and destruction is because there is no need to zero out dummy if it's a primitive type like an int. This class does that naturally. (But, the use of creating an anonymous temporary using Name{} will cause it to be zero initialized. I think you can suppress that by declaring a defaulted default constructor, at least for some versions of C++. Normally we expect zero initialization for primitives when you use {} so I would leave that behavior in to prevent surprises.)

a different approach

You have an rvalue and want to pass it to a parameter that wants an lvalue reference. This is very similar to the case where you have an lvalue and want to pass it to a parameter that wants an rvalue reference. That is, you just want to change the value category.

The latter is done with std::move, and it's implemented as nothing more than a cast. The function template std::forward is also a cast, and will produce either an lvalue or rvalue reference.

So, why not do the same thing?

If you wrote the opposite of move, you would still need to create the temporary using the normal syntax. This would look like:

foo(lvalue(X{}));

which has the advantage that you can give constructor arguments in the normal way, or nominate any temporary such as the result of another expression. That can be useful for "in/out" parameters where you don't care about the "out". E.g. lvalue(complex(2,5)). It's also clear what you are doing and easy to read.

If you want to avoid the extra {} you still need to specify the type somewhere. Make it a function that takes a default argument, and then you can optionally write it as lvalue<T>() instead.

\$\endgroup\$
6
  • \$\begingroup\$ Thanks for your answer! I'd totally overlooked the fact that I missed calling the destructor. Things like this were exactly why I posted my code here, thanks :-) There's nothing really wrong with your suggestion, I was just looking for a "cleaner" way to write it without having to the declare the variable beforehand. Skipping the default ctor probably really doesn't do much good, so I'll just call it, regardless of whether the type is trivially copyable or not. \$\endgroup\$ Sep 22 at 17:38
  • \$\begingroup\$ @L.F. But I did call the constructor, I just forgot to call the destructor. So std::string will have been constructed correctly, or am I mistaken? \$\endgroup\$ Sep 23 at 10:10
  • \$\begingroup\$ @MatthiasGrün Sorry, I was looking at the wrong section of the code ... \$\endgroup\$
    – L. F.
    Sep 23 at 11:04
  • 1
    \$\begingroup\$ @MatthiasGrün ...but why? Just make a member of the class in the ordinary way; no need for the placement new and all that. The magic is in the operator T& that lets you get away with making a temporary in sito, which your comment tells me is the actual point of all this. \$\endgroup\$
    – JDługosz
    Sep 23 at 17:14
  • \$\begingroup\$ @MatthiasGrün I've updated my answer to elaborate on some other approaches for you. \$\endgroup\$
    – JDługosz
    Sep 24 at 14:02

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service, privacy policy and cookie policy

Not the answer you're looking for? Browse other questions tagged or ask your own question.