C++ gives the programmer extremely broad power over data types and how they behave. You can create a custom type and define whether and how it can be added, multiplied, compared, iterated, printed as output, called as a function, transformed into another type—all of which then interacts with the C++ language and enables its use elsewhere.
Developers of other languages whine and cope about this, notably Java maintainers calling it too confusing to read code where custom types can be added together; it's nonsense at best, if not an insult in assuming people aren't intelligent enough for that feature. A type representing a 3D vector can have a perfectly clear meaning associated with adding them together. As usual, to stand against that because it
could be abused is yet another case of trying to protect programmers from themselves, a mistake which so many maintainers just cannot resist making.
That is, especially when abusing it is such a good way to demonstrate it! Suppose we are creating a new type to represent integers for nigger math, which I'll call "nigint" for nigger integer. We can start by making a "wrapper" which stores a normal integer:
class nigint
{
private:
int i;
public:
nigint(int i=0) : i(i) {}
};
The
private section stores data and functions only to be accessible internally, so outside interaction will be forced to work via definitions we'll add to the
public section. That public function is a
constructor which stores a normal int directly, and has an implicit return-type (a finished object of our
nigint type). The default value of zero allows for omitting it, in effect defining two constructors with and without the parameter, which has various compatibility advantages. The braces are an empty constructor body, because there isn't anything else to do and it cannot be absent.
Good so far, albeit lacking interaction and functionality, so code won't yet compile if it attempts to use or output one of these. At minimum, we can tell the compiler that our type can at least be
cast to an integer by defining the corresponding
operator, with this placed in the
public section:
operator int() const {return i;}
The keyword
operator is used for defining functions which interact with the language. Cast-operations are special in also having an implicit return type, but otherwise the syntax is just like a normal function, including
const to indicate that the function does not modify our object.
A quick test before continuing:
#include <iostream>
class nigint
{
private:
int i;
public:
nigint(int i=0) : i(i) {}
operator int() const {return i;}
};
int main(int, char✱✱)
{
nigint ni(4);
std::cout << ni << "\n";
}
That at least outputs "4" rather than failing to compile. Actually, commenting out the operator definition can be illustrative here, it causes this compile error:
no match for ‘operator<<’ (operand types are ‘std::ostream’ {aka ‘std::basic_ostream<char>’} and ‘nigint’)
Ultimately the
iostream interface wants a way to output a
nigint using the << syntax, but it's happy without that as long as it has an implicit way available, which the cast to
int provides.
Niggers don't know how to write or speak proper numbers though, so let's actually implement that operator to control directly what it does for our type. We need a function header for
operator<< which takes references to a
std::ostream and a
nigint, and returns the
std::ostream reference back. We also need access to the underlying
int though, so first declare this function within the
nigint class as a "friend":
friend std::ostream& operator<< (std::ostream& os, const nigint& ni);
This is a deliberate mechanism to give that function access to the private members within our type, in this case the underlying
int value. Now we can implement it outside the class, using full access:
std::ostream& operator<< (std::ostream& os, const nigint& ni)
{
switch (ni.i)
{
case 0: os << "no"; break;
case 1: os << "wuh"; break;
case 2: os << "tu"; break;
case 3: os << "tree"; break;
case 4: os << "fo"; break;
case 5: os << "fav"; break;
case 6: os << "seek"; break;
case 7: os << "sen"; break;
case 8: os << "aid"; break;
case 9: os << "nan"; break;
default: os << "sheeeit";
}
return os;
}
Placing that just after the
nigint class, the previous program now outputs "fo". The return reference there is just part of how
iostream works, allowing multiple
<< operations to be chained together. Note that the attempt to access
ni.i only works because the function was declared as a friend, otherwise the compiler error will say
private within this context.
Now we can use the type as a number
and impose that output logic:
int main(int, char✱✱)
{
nigint ni(4);
std::cout << ni << "\n";
std::cout << nigint(ni + 3) << "\n";
}
Output is "fo" and "sen", as expected. The expression
ni + 3 is using the implicit cast to
int, for which the result will be another
int and the normal output would be "7", but we're telling the compiler to make a temporary
nigint instance of it first before the output stream gets to see it.
Niggers also can't add properly though. No problem; we can also define what happens when we add to a
nigint object. This we can do entirely within the
nigint class:
nigint operator+ (int x) {return i + x + 1;}
In this case, the object itself is the value to the left when the operator is used, and the argument
x is the value to the right. The return expression is just three normal integers added together, which the compiler casts implicitly to
nigint due to the function return type, and it knows how to do that because the correct constructor exists.
Because that operation returns a
nigint, we can now also test the summation directly without the explicit constructor:
int main(int, char✱✱)
{
nigint ni(4);
std::cout << ni << "\n";
std::cout << (ni + 3) << "\n";
}
Now we get "fo" and "aid", great. Some niggers know at least a few of the tens-place numbers though, let's add those into the
operator<< overload. We can check for some known cases and output those words first:
std::ostream& operator<< (std::ostream& os, const nigint& ni)
{
int i = ni.i;
if (i >= 40 && i < 50) {os << "fody"; i -= 40;}
else if (i >= 50 && i < 60) {os << "fiddy"; i -= 50;}
switch (i)
{
case 0: os << "no"; break;
case 1: os << "wuh"; break;
case 2: os << "tu"; break;
case 3: os << "tree"; break;
case 4: os << "fo"; break;
case 5: os << "fav"; break;
case 6: os << "seek"; break;
case 7: os << "sen"; break;
case 8: os << "aid"; break;
case 9: os << "nan"; break;
default: os << "sheeeit";
}
return os;
}
This is where the
const protection is good to keep in mind; the operator is not allowed to modify the object being printed, but it can use that read-only access to make a mutable copy of the integer. That then lets us subtract off any part of it accounted first before handing the remainder to the
switch statement.
Test:
int main(int, char✱✱)
{
nigint ni(4);
std::cout << ni << "\n";
std::cout << (ni + 40) << "\n";
std::cout << nigint(53) << "\n";
std::cout << (ni + ni + ni) << "\n";
}
Output:
fo
fodyfav
fiddytree
sheeeit
If there's one way that nigger math is useful, it's being an example!
SithEmpire 0 points 2 weeks ago
It should have occurred to me sooner - examples are normally simple enough that they become unrealistic in their context, and the cure is to use a mentally deficient context!
I also had the idea of a floating point type called "chosen" which halves every time you read it, but there was something more special about refusing to count above 9.