5

I'm writting a simple C++ HTTP server framework. In my Server class, I can add Route's. Every route consists of a path, an HTTP method and a Controller (which is a pipeline of functions to be called when the request was made.) That Controller class is constructed by receiving a list of std::function's (or, more precisely: std::function<void(const HTTPRequest&, HTTPResponse&, Context&)>), but most of the time (or I should say every time), this Controller will be initialized with a list of lambda function literals, as in the following code:

server.add_route("/", HTTPMethod::GET,
                {
                    [](auto, auto& response, auto&) {
                        const int ok  = 200;
                        response.set_status(ok);
                        response << "[{ \"test1\": \"1\" },";
                        response["Content-Type"] = "text/json; charset=utf-8";
                    },
                    [](auto, auto& response, auto&) {
                        response << "{ \"test2\": \"2\" }]";
                    },
                }
        );

Being this the case, I would like to make the add_route function a constexpr, because, correct me if I am wrong, constexpr functions can be executed at compile time.

So, when I was making everything constexpr, I found the following error:

Controller.cpp:9:1 constexpr constructor's 1st parameter type 'Callable' (aka 'function<void (const HTTPRequest &, HTTPResponse &, Context &)>') is not a literal type

What I want to know is: why std::function's can't be literal types? Is there any way to circumvent this limitation?

Below is the code for Controller class. I'm aware that there are still other compile errors, but this is the main issue I'm tackling right now. Thanks in advance!

controller.hpp

#pragma once

#include <functional>
#include <initializer_list>
#include <vector>

#include "context.hpp"
#include "httprequest.hpp"
#include "httpresponse.hpp"

typedef std::function<void(const HTTPRequest&, HTTPResponse&, Context&)> Callable;
template <size_t N>
class Controller {
private:
    std::array<Callable, N> callables;

public:
    static auto empty_controller() -> Controller<1>;

    constexpr explicit Controller(Callable);
    constexpr Controller();
    constexpr Controller(std::initializer_list<Callable>);

    void call(const HTTPRequest&, HTTPResponse&, Context&);
};

controller.cpp

#include "controller.hpp"

template <size_t N>
auto Controller<N>::empty_controller() -> Controller<1> {
    return Controller<1>([](auto, auto, auto) {});
}

template <>
constexpr Controller<1>::Controller(Callable _callable) :
    callables(std::array<Callable, 1> { std::move(_callable) }) { }

template <>
constexpr Controller<1>::Controller() :
    Controller(empty_controller()) { }


template <size_t N>
constexpr Controller<N>::Controller(std::initializer_list<Callable> _list_callables) :
    callables(_list_callables) { }

template <size_t N>
void Controller<N>::call(const HTTPRequest& req, HTTPResponse& res, Context& ctx) {
    for (auto& callable : callables) {
        callable(req, res, ctx);
    }
}
  • Yes, `std::function` is not a literal type. e.g. it has no `constexpr` constructors. Perhaps you could pass a function pointer instead, which should work. – cigien Sep 06 '20 at 20:39
  • I considered to use function pointers at first, but then I found out they wouldn't allow me to make the `add_route` syntax look as clean as I have managed to make. Aside from that, as I discovered from the accepted answer, making `add_route` `constexpr` probably wouldn't even be possible, or likely wouldn't be an useful optimization, since the method necessarily has to alter the state of the `Server` class. – André Winston Sep 06 '20 at 21:36

1 Answers1

5

why std::function's can't be literal types? Is there any way to circumvent this limitation?

Because it uses type erasure in order to accept any callable. This requires polymorphism which cannot be constexpr until C++20 which will allow constexpr virtual. You could use templates and capture the callable directly, but its type will creep into Controller and spread further.

Being this the case, I would like to make the add_route function a constexpr, because, correct me if I am wrong, constexpr functions can be executed at compile time.

Yes, if given constexpr arguments, the function will be executed at compile-time. Look at it like advanced constant folding. Furthermore constexpr methods used in compile-time context either cannot access *this or it has too be constexpr. In particular, constexpr method can only change the state of constexpr instance at compile time. Otherwise the function is run ordinarly at runtime.

The last point is relevant to you, running a HTTP server at compile-time hardly makes sense, so constexpr is probably not needed and it won't help anything.

EDIT constexpr behaviour example

struct Foo{
    //If all members are trivial enough and initialized, the constructor is constexpr by default.
    int state=10;
    //constexpr Foo()=default;
constexpr int bar(bool use_state){
    if(use_state)
        return state++;
    else
        return 0;// Literal
}
constexpr int get_state()const{
    return state;
}
};

template<int arg>
void baz(){}
int main(int argc, char* argv[])
{
   Foo foo;
   //Carefull, this also implies const and ::bar() is non-const.
   constexpr Foo c_foo;

   foo.bar(true);//Run-time, `this` is not constexpr even though `true` is
   foo.bar(false);//Compile-time, `this` was not needed, `false` is constexpr

   bool* b = new bool{false};
   foo.bar(*b);//Always run-time since `*b` is not constexpr



   //Force compile-time evaluation in compile-time context
   //Foo has constexpr constructor, creates non-const (temporary) constexpr instance
   baz<Foo().bar(true)>();
   baz<Foo().bar(false)>();
   baz<foo.bar(false)>();
   //ERROR, foo is not constexpr
   //baz<foo.bar(true)>();
   //ERROR, c_foo is const
   //baz<c_foo.bar(false)>();
   //Okay, c_foo is constexpr
   baz<c_foo.get_state()>();
   //ERROR, foo is not constexpr
   //baz<foo.get_state()>();

    return 0;
}
Quimby
  • 17,735
  • 4
  • 35
  • 55
  • So even though every parameter inside `add_route` is a `constexpr`, the compiler wouldn't allow me to add the `constexpr` specifier for that method just because it needs to modify the state of the `Server` object? – André Winston Sep 06 '20 at 21:30
  • @AndréWinston It always allows adding it, it will just fallback to run-time execution on call. The key idea is that `constexpr` forces compile-time execution **only if supplied with constexpr arguments or in compile-time context(where the arguments are also constexpr)**. It does not forbid deferring the execution to run-time as usual. The state access is on need-to-know basis. I.e. accessing state in not-taken branch is still okay even in compile-time call. I added an example to my answer. Feel free to ask for more :) – Quimby Sep 07 '20 at 08:41
  • If you are interested, look at C++20 `consteval` which always runs at compile-time. – Quimby Sep 07 '20 at 08:55