问题
I'm trying to write a demo that implements the fmap
in Haskell with continuation
, and my code looks like this:
#include <cstdio>
#include <functional>
template <typename X>
using Callback = std::function<void(X)>;
template <typename X, typename Y>
using Fun = std::function<Y(X)>;
template <typename X, typename Y>
struct F_map;
template <typename X>
struct __F {
virtual void operator()(Callback<X>&& callback) = 0;
virtual __F<X>* self() { return this; }
template <typename Y>
auto map(Fun<X, Y>&& f) { return F_map(self(), f); }
};
template <typename X>
struct F_id : __F<X> {
const X x;
F_id(const X& x) : x(x) {}
__F<X>* self() override { return this; }
void operator()(Callback<X>&& callback) override { callback(x); }
};
template <typename X, typename Y>
struct F_map : __F<Y> {
__F<X>* upstream;
Fun<X, Y> f;
F_map(__F<X>* upstream, const Fun<X, Y>& f) : upstream(upstream), f(f) {}
__F<Y>* self() override { return this; }
void operator()(Callback<Y>&& callback) override {
upstream->operator()([=](X&& x) {
callback(f(x));
});
}
};
int main(int argc, char* argv[]) {
auto f =
F_id(10)
.map<int>([](int x) { return x + 2; })
.map<const char*>([](int x) { return "1, 2, 3"; });
f([](const char* x) { printf("%s\n", x); });
return 0;
}
That works fine, but the map<int>
and map<const char*>
looks ugly. I think these declarations can be omitted, but if I remove that I got an error message that says "no instance of function template "F_id::map [with X=int]" matches the argument list".
Any idea to remove these template arguments?
回答1:
There are multiple kinds of polymorphism in C++. By polymorphism, I mean whenever a single variable in code has different implementation types.
There is classic C++ inheritance and virtual based polymorphism. There is type erasure based polymorphism. And there is static polymorphism of templates.
In many senses, these kinds of polymorphism are opposed to each other. If you use one when you should be using the other, it is like using covariance when you should be using contravariance. Your code might stumble along, but it will only work when forced, like taking a square peg and a round hole and a big hammer.
Your <int>
requirement is an example of using the wrong kind of polymorphism, and the <int>
is the hammer smashing it into the wrong-shaped hole.
You are attempting to use
template <typename X>
using Callback = std::function<void(X)>;
and
template <typename X, typename Y>
using Fun = std::function<Y(X)>;
as pattern matchers. They aren't pattern matchers, even if in specific cases they can be used as pattern matchers. Callback
and Fun
are a type erasers.
Callback<X>
takes anything that can be called with something that can be converted from an X
, and stores it. Then forgets almost every other fact about it (well, it remembers how to copy it, its typeid, and a few other random facts).
Fun<X,Y>
takes anything that can be called with something that can be converted from an X
, and whose return value can then be converted to a Y
. It then forgets almost every other fact about it.
Here:
template <typename Y>
auto map(Fun<X, Y>&& f) { return F_map(self(), f); }
you are trying to use it to say "I accept an f
. Please find me a Y
that would match this f
".
This is pattern matching. Type erasure and pattern matching are opposite operations.
This is a really common mistake. With classic inheritance, they sometimes end up being the same thing.
std::function
is for forgetting information about something, being able to store it, then later using only the parts you remember.
The first question is, do you need to pattern match, or do you need a type function here?
Probably you are good with a type function.
template <class F, class R = std::invoke_result_t<F, X>>
F_map<X,R> map(F&& f) { return {self(), std::forward<F>(f)}; }
here we map the incoming F
to its return value R
.
Your code has other issues. Like dangling pointers. Also, it insists on knowing what types the callables use; in C++, you can ... just not bother knowing that.
So, using the CRTP for static polymorphism, and mechanically forgetting what types I work on an replacing them with non-type erased code, I get:
#include <cstdio>
#include <type_traits>
template <class Upstream, class F>
struct F_map;
template<class D>
struct mappable
{
template <class F>
F_map<D, F> map(F const& f) { return F_map(static_cast<D*>(this), f); }
};
template <class Upstream, class F>
struct F_map:
mappable<F_map<Upstream, F>>
{
Upstream* upstream;
F f;
F_map(Upstream* upstream, const F& f) : upstream(upstream), f(f) {}
template<class Callback>
void operator()(Callback&& callback) {
(*upstream)([=](auto&& x) {
callback(f(decltype(x)(x)));
});
}
};
template <typename X>
struct F_id:
mappable<F_id<X>>
{
const X x;
F_id(const X& x) : x(x) {}
template<class Callback>
void operator()(Callback&& callback) { callback(x); }
};
int main(int argc, char* argv[]) {
auto f =
F_id(10)
.map([](int x) { return x + 2; })
.map([](int x) { return "1, 2, 3"; });
f([](const char* x) { printf("%s\n", x); });
return 0;
}
Live example.
I still think you are following dangling pointers, but I am not sure.
The return value of map
stores a pointer to the object we call it on, and that object was a temporary destroyed when we made f
.
To fix the Upstream*
problem, I'd do this:
template <class Upstream, class F>
struct F_map;
template<class D>
struct mappable
{
template <class F>
F_map<D, F> map(F const& f) const { return {*static_cast<D const*>(this), f}; }
};
template <class Upstream, class F>
struct F_map:
mappable<F_map<Upstream, F>>
{
Upstream upstream;
F f;
F_map(Upstream const& upstream, const F& f) : upstream(upstream), f(f) {}
template<class Callback>
void operator()(Callback&& callback) const {
upstream([=](auto&& x) {
callback(f(decltype(x)(x)));
});
}
};
template <typename X>
struct F_id:
mappable<F_id<X>>
{
const X x;
F_id(const X& x) : x(x) {}
template<class Callback>
void operator()(Callback&& callback) const { callback(x); }
};
copy upstream
by value.
回答2:
If you don't want to provide template arguments to __F::map
, you can add another map
template that accepts an arbitrary callable, and you can use the type of that callable to figure out the return type:
template <typename F> // <-- any function
auto map(F&& f) { return F_map<X, decltype(f(X{}))>(self(), f); }
// ^ ^^^^^^^^^^^^^^^^ argument, and return type
Note that you now need to specify the template parameters to F_map
, but you have the needed information to do that, and is not something the caller has to worry about.
Now this call works:
auto f =
F_id(10)
.map([](int x) { return x + 2; })
.map([](int x) { return "1, 2, 3"; });
Here's a demo.
As pointed out by Marek R in a comment, and as apparent from the demo with Clang, you have some bug in your code that causes a run-time error. You should fix that.
Also, as pointed out by Yakk-Adam Nevraumont in a comment, names containing double underscores __
are reserved for the implementation. Don't use such names in your code.
来源:https://stackoverflow.com/questions/65345054/eliminate-redundant-template-argument-in-c