Reputation: 131808
In a bunch of code that I'm writing I want to indicate that certain variables are to be used in a certain way, or have a certain characteristic to them. For the sake of discussion, suppose variables can be sweet, salty, sour or bitter.
What I use now is something like:
int foo() {
int salty_x;
int sour_y;
do_stuff_with(salty_x,sour_y);
}
And I might also have sour_x
, or salty_y
etc.
Ideally - but this is not valid C++ - I would have been able to write something like this:
int foo() {
namespace salty { int x; }
namespace sour { int y; }
do_stuff_with(salty::x,sour::y);
}
and this would nicely allow for a "sour x" and a "salty x" in the same function - if this syntax had been valid of course.
This may remind you of Hungarian Notation, except that it's not about variable types or sizes, and that the saltiness or sourness etc. are not inherent in x or in y - they only describe the way they're used.
Now, you could ask: "Ok, why not just put these in struct
's?", that is, why not do:
int foo() {
struct { int x; } salty;
struct { int y; } sour;
do_stuff_with(salty.x,sour.y);
}
But that preculdes defining additional salty/sour variables; and if I bunch them all at the beginning of the function, C-style, then it looks as though I indicate the variables are related, which is not necessarily the case.
What I currently do is just prefix the names: salty_x
, sour_y
. It's ugly but it works.
My question: Is there something else I could do which would look closer, in use, to my desired code, and at the same time not require too much "coding gymnastics"?
Due to popular demand: As a motivating/concretizing example, "salty" might mean "is uniform across all threads in a GPU warp, but possibly not across different warps" and "sour" might mean "is uniform across all threads in a CUDA kernel grid block / OpenCL workgroup when they reach this point in the code". But this is not a question about GPUs, CUDA, or OpenCL.
Upvotes: 4
Views: 419
Reputation: 1302
Postfixes.
They usually hamper readibility a lot less than prefixes, while maintaining the same hinting quality.
Yes I know it's obvious, but it's very possible that it hadn't come to your mind.
And mind that the Hungarian Notation was originally meant to be employed pretty much as you would in your case; see "Apps Hungarian" in that wikipedia entry.
Upvotes: 0
Reputation: 13988
I'm sure you've checked all conventional approaches and neither was satisfactory... Lets turn to the magic then to achieve (I think) exactly what you want (c++17 will be needed):
#include <iostream>
#include <type_traits>
#include <variant>
#include <utility>
#include <typeinfo>
#include <typeindex>
#include <map>
template <auto Label>
using ic = std::integral_constant<decltype(Label), Label>;
template <class... Ts>
struct context {
template <auto Label, auto (*Namespace)(std::integral_constant<decltype(Label), Label>)>
decltype(Namespace(ic<Label>{}))& get() {
try {
return std::get<decltype(Namespace(std::integral_constant<decltype(Label), Label>{}))>(values[typeid(std::pair<std::integral_constant<decltype(Namespace), Namespace>, std::integral_constant<decltype(Label), Label>>)]);
} catch (std::bad_variant_access&) {
values[typeid(std::pair<std::integral_constant<decltype(Namespace), Namespace>, std::integral_constant<decltype(Label), Label>>)] = decltype(Namespace(std::integral_constant<decltype(Label), Label>{})){};
}
return std::get<decltype(Namespace(std::integral_constant<decltype(Label), Label>{}))>(values[typeid(std::pair<std::integral_constant<decltype(Namespace), Namespace>, std::integral_constant<decltype(Label), Label>>)]);
}
std::map<std::type_index, std::variant<Ts...>> values;
};
int main(){
enum { x }; // defining label x
int salty(ic<x>); // x is an int in salty namespace
enum { y }; // defining label y
float sour(ic<y>); // y is a float in sour namespace
context<int, float, char> c;
c.get<x, salty>() = 2;
c.get<y, sour>() = 3.0f;
char sour(ic<x>); // x is a char in sour namespace
c.get<x, sour>() = 'a';
std::cout << "salty::x = " << c.get<x, salty>() << std::endl;
std::cout << "sour::y = " << c.get<y, sour>() << std::endl;
std::cout << "sour::x = " << c.get<x, sour>() << std::endl;
}
One thing to be mentioned - gcc doesn't like the code though according to standard it should: [see this] [and that].
Upvotes: 1
Reputation: 13269
If I understand your edit correctly, you want to be able to define variables that behave exactly like int
or float
variables, but retain additional, ideally compile-time, information about their types.
The only thing I can't help you with is this:
Sometimes I even want to have a "sour x" and a "salty x" in the same function, which I could do if this syntax had been valid.
Personally, I would just prefix the variable name.
Anyway, here is an example of what you can do.
enum class Flavor {
Salty, Sour
};
template <typename T, Flavor f>
struct Flavored {
using Type = T;
static constexpr Flavor flavor = f;
T value;
Flavored(T v) : value(v) {}
operator T() { return value; }
};
And here is an example of how to use it.
Upvotes: 0
Reputation: 24412
The hardest constraint was
Sometimes I even want to have a "sour x" and a "salty x" in the same function
So - the solution is the first usage of variadic template template parameter I ever made - so, here you are:
template <typename T>
struct salty
{
T salty;
};
template <typename T>
struct sour
{
T sour;
};
template <typename T, template <typename> class ...D>
struct variable : D<T>...
{};
And the usage:
salty<int> x;
x.salty = 5;
variable<int, salty, sour> y;
y.sour = 6;
y.salty = 5;
Upvotes: 1