I am trying to implement a C++ Translate function for localization.
// Language package, containing key-value pairs of translation, e.g.,
// g_LanguagePack["HELLO@1"] = "Hello, {}!"
// The "@N" suffix indicates that this format string has N parameters.
// When there is no parameter, the suffix can be omitted.
std::unordered_map<std::string, std::string> g_LanguagePack;
// The translator function
template <typename VA...>
std::sting Translate(const std::string& key, VA&&... params);
When invoked, e.g., Translate("HELLO@1", "FOO") will do a lookup in the language package and return the localized string "Hello, FOO!".
key is guaranteed to be a compile-time string (so key's type may need to be changed), and in practice developers may provide mismatching number of parameters, or missing @N while providing parameters. So I think it is necessary to add a check mechanism to ensure N == sizeof...(VA).
At the beginning, I used static_assert in Checker, and it failed because static assertion expression is not an integral constant expression. Then I learned User-defined literal string: compile-time length check that I can directly use assert in consteval functions, and it works.
However, the GPT said it's not recommended to call assert in consteval functions (I am not sure about it, since it compiles on both Clang and MSVC). And, if it is not recomended, what could be a better implementation?
template <std::size_t N, typename... VA>
consteval void Checker(const char (&key)[N], VA&&... va)
{
std::string_view string_view(key, N - 1);
std::size_t param_cnt = 0;
auto indicator_index = string_view.find('@');
if (indicator_index == std::string_view::npos) // no params
{
assert(sizeof...(VA) == 0);
}
else
{
// parse param_cnt_
for (auto i = indicator_index + 1; string_view.begin() + i != string_view.end(); i++)
{
auto digit = string_view.at(i); // get digit
assert('0' <= digit && digit <= '9');
param_cnt = param_cnt * 10 + digit - '0';
}
assert(sizeof...(va) == param_cnt);
}
}
int main(int argc, char* argv[])
{
Checker("foo@2", 1, 2);
Checker("foo@1", "string");
return 0;
}
Then comes the tricky part. I tried to use Checker in Translate, unfortunately, it did not work. It's because key, when passed as parameter, is no longer guaranteed to be a compile-time constant.
template <std::size_t N, typename... VA>
std::string Translate(const char (&key)[N], VA&&... va)
{
Checker(key, va...); // Function parameter 'key' with unknown value cannot be used in a constant expression
// do translation
return "";
}
LocalString<1> hello_foo_key={42};This also lets you move the key lookup to link time instead of runtime + memory. Even better:LocalString<std::string, int> hello_name_and_count_key = {43};makes it trivial to ensure the developer passed arguments of the right types.Translateis always called with a literalkey, andkeycontains param count information, what I want to do is to mimic the compile-time check facility ofstd::formatwhile maintaining compatibility so thatTranslate("HELLO@1", "FOO");still works.consteval/constexprfunctions areinline, andinlinefunctions shouldn't use assert because it might be an ODR violation unless you ensureNDEBUGis the same before you#include <cassert>every time the function is compiled. Also, it just won't error ifNDEBUGis set for yourassertmacro