I had a random thought while drinking my Friday coffee. This is not a proposal. I discussed it on a C++ Discord, but it was met with criticism. That said, sometimes even the biggest failures can lead to interesting successes, so I'm curious what the std-discussion list thinks about it.
The concept is compile-time data, which I'm calling "consteval members". Its goal is to simplify some of the situations where templates and macros are abused by letting people express their intent in a more literal, OOP fashion using data that the compiler cannot allow in the actual assembly. This can be used as better control flow based on the specific value of literals, and it can be used for better compile-time error handling.
The key is that the transforms must happen at compile-time. The consteval data cannot be in the application at runtime.
Some concerns:
On the plus side, I also believe that it allows very readable and maintainable code, especially for library authors that hide its usage with access specifiers, so it might lead into some interesting ideas.
This email will show a use case where a factory-style object
collects properties and produces an object instance ("the builder
pattern"). This mechanism will allow the various functions to
guide the user (with compiler errors) if they forgot required
data, or if they set incompatible properties. In this case, the
user must supply an IP address and a port, but they are allowed to
choose any mechanism that does that, and (in this example) they
can do so in any order (although another use case could easily
check for order).
class BuildableEndpoint {
consteval bool hasAddress; //Cannot be used at runtime.
Does not appear in runtime object.
consteval bool hasPort; // Cannot be used at runtime.
Does not appear in runtime object.
std::unique_ptr<InternetAddress> address;
uint16_t port;
// I'm guessing constexpr qualifier
would be necessary if the argument affected the consteval
members.
BuildableEndpoint& AddIpV6AddressFromString(std::string
argAddress)
{
static_assert(hasAddress == false, "Endpoint was already
assigned an IP address.");
address = ParseIpV6Address(argAddress);
hasAddress = true;
return *this;
}
BuildableEndpoint& AddIpV4AddressFromString(std::string
argAddress)
{
static_assert(hasAddress == false, "Endpoint was already
assigned an IP address.");
address = ParseIpV4Address(argAddress);
hasAddress = true;
return *this;
}
BuildableEndpoint& AddIpV4AddressFromMask(uint8_t A,
uint8_t B, uint8_t C, uint8_t D)
{
static_assert(hasAddress == false, "Endpoint was already
assigned an IP address.");
address = std::make_unique<SomeIpV4Address>(A, B, C,
D);
hasAddress = true;
return *this;
}
BuildableEndpoint& AddPort(uint16_t argPort)
{
static_assert(hasPort == false, "Endpoint was already
assigned a port.");
port = argPort;
hasPort = true;
}
MyEndpoint Build()
{
static_assert(hasAddress, "Endpoint must have an address
before building."); //OK
static_assert(hasPort, "Endpoint must be assigned a port
before building."); //OK
return MyEndpoint(address, port);
}
};
int main()
{
// This is okay. It has both an address (IPv4 or IPv6 doesn't
matter) and a port.
MyEndpoint endpoint1 = BuildableEndpoint()
.AddIpV4AddressFromString("10.45.20.12")
.AddPort(1022)
.Build();
// This is also okay.
MyEndpoint endpoint2 = BuildableEndpoint()
.AddIpV4AddressFromMask(10, 45, 20, 12)
.AddPort(1022)
.Build();
// This will
trigger the hasPort static assert in Build() and not compile.
// MyEndpoint endpoint = BuildableEndpoint()
// .AddIpV4AddressFromMask(10, 45, 20, 12)
// .Build();
}