12
\$\begingroup\$

I've implemented an identity function (well, actually a functor struct) in C++. The goal is that every occurrence of an expression expr in the code can be substituted by identity(expr) without changing the semantics of the program at all.

I make it a struct with a templated operator() instead of simply a function template because this enables us to pass identity as an argument to other functions (say std::transform) without needing to specify the type (e.g., identity<std::pair<std::vector<int>::const_iterator, std::vector<double>::const_iterator>>).

The code is written in C++14.

#include <utility>

static constexpr struct identity_t {
    template<typename T>
    constexpr decltype(auto) operator()(T&& t) const noexcept
    {
        return std::forward<T>(t);
    }
} identity{};

Usage:

int x = 42;
identity(x) = 24;

constexpr int size = 123;
int arr[identity(size)] = {};

vector<int> v1 = {1, 2, 3, 4}, v2;
v2.reserve(v1.size());
transform(v1.cbegin(), v1.cend(),
          back_inserter(v2),
          identity);

template<typename T>
class my_container {
public:
    template<typename F>
    auto flat_map(F&& f) const { ... }

    auto flatten() const { return flat_map(identity); }

    ...
};

Since identity(expr) is nothing more than a cast, performance should not be a problem. I'm concerned whether there are cases in which this implementation fails to keep the program's behavior unchanged. If there are, how can I fix the implementation?

EDIT: I changed "a variable x" in the first paragraph to "an expression expr". The latter is more accurate.

\$\endgroup\$
  • 2
    \$\begingroup\$ Those ...'s in your code suggest that there is more to your implementation, which you need to include. Right now this question is off topic as it is basically stub code. \$\endgroup\$ – syb0rg Jul 12 '16 at 12:42
  • 1
    \$\begingroup\$ Does it work as intended even without the examples (those cut away with the ...)? \$\endgroup\$ – Mast Jul 12 '16 at 12:56
  • 1
    \$\begingroup\$ @Mast I'm not exactly sure what you mean; identity itself does work. \$\endgroup\$ – Zizheng Tai Jul 12 '16 at 12:57
  • 1
    \$\begingroup\$ @ZizhengTai I think they're fine as is, as long as the code you're looking to have reviewed is complete and works as intended. \$\endgroup\$ – syb0rg Jul 12 '16 at 13:01
  • 1
    \$\begingroup\$ I think if you've got an overloaded function or function template which you pass to a function pointer argument, you won't be able to insert identity. However that's a limitation of the language that cannot be avoided. \$\endgroup\$ – celtschk Jul 13 '16 at 9:31
4
\$\begingroup\$

Well, that looks like a straightforward use of perfect forwarding in the parameter (T&&, std::forward<T>) and in the return type (decltype(auto)). So yep, you've got it.

There are plenty of places in C++ where x and identity(x) don't do the same thing, either because x is a name as well as an expression, or because x is a construct that looks like an expression but technically is not one. (Personally, I consider almost all of these places to be mistakes in the design of C++.) Here are the ones I can think of.

Replacing identity(x) with x in all of the following examples changes the behavior of the code sample.


As mentioned in the comments on the question, the name of a function template can deduce what kind of function pointer to decay to:

template<class T> void f(T) {}
void (*fp)(int) = identity(f);

The name of a local variable gets copy elision (a.k.a. Named Return Value Optimization, or NRVO):

struct S {
    S() {}
    S(const S&) { puts("copy"); }
    S(S&&) { puts("move"); }
};
S f() {
    S s;
    return identity(s);
}
int main() { f(); }

The name of a local variable is treated as an rvalue in the context of return:

struct S {
    S() {}
    S(const S&) { puts("copy"); }
    S(S&&) { puts("move"); }
};
std::any f() {
    S s;
    return identity(s);
}
int main() { f(); }

Lifetime extension applies to temporaries whose construction is "visible" in some sense that is extremely subtle when I start thinking hard about it. Interposing the call to identity disables lifetime extension.

struct S {
    S() { puts("ctor"); }
    S(const S&) = delete;
    S(S&&) = delete;
    ~S() { puts("dtor"); }
};
int main() {
    const S& x = identity(S());
    puts("done");
}

Braced initializers and string literals look like expressions, but in the context of an initializer, they're special cases:

const char x[] = identity("hello");
int y = identity({5});

Now, I can't imagine how you'd use identity in your codebase where any of these quirks might matter; but then, I don't quite see how you plan to use identity at all. Your only fleshed-out example is just using std::transform(..., identity) as a verbose way of writing std::copy(...), which doesn't seem very useful.

\$\endgroup\$

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.