HiveBrain v1.2.0
Get Started
← Back to all entries
patterncppMinor

C++ identity function

Submitted by: @import:stackexchange-codereview··
0
Viewed 0 times
identityfunctionstackoverflow

Problem

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::const_iterator>>).

The code is written in C++14.

#include 

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


Usage:

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

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

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

template
class my_container {
public:
    template
    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.

Solution

Well, that looks like a straightforward use of perfect forwarding in the parameter (T&&, std::forward) 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 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.

Code Snippets

template<class T> void f(T) {}
void (*fp)(int) = identity(f);
struct S {
    S() {}
    S(const S&) { puts("copy"); }
    S(S&&) { puts("move"); }
};
S f() {
    S s;
    return identity(s);
}
int main() { f(); }
struct S {
    S() {}
    S(const S&) { puts("copy"); }
    S(S&&) { puts("move"); }
};
std::any f() {
    S s;
    return identity(s);
}
int main() { f(); }
struct S {
    S() { puts("ctor"); }
    S(const S&) = delete;
    S(S&&) = delete;
    ~S() { puts("dtor"); }
};
int main() {
    const S& x = identity(S());
    puts("done");
}
const char x[] = identity("hello");
int y = identity({5});

Context

StackExchange Code Review Q#134627, answer score: 7

Revisions (0)

No revisions yet.