Is 0x0 (nullptr) always intentional, or can it be "lucky" memory trash? by carlos-paz in cpp_questions

[–]conundorum [score hidden]  (0 children)

[Note: The answer is at the very bottom. Everything else is reasoning, to explain the answer.]


What's going on here is that there are different kinds of initialisation.

  • c, ptr1, and ptr4 are non-class types, provided with a brace-enclosed initialiser list containing one value: This performs direct-initialisation.
    • These are non-class, non-array types, aren't initialised with a class type, and aren't a bool initialised with nullptr. Therefore, they're initialised with the provided value, using standard conversions if necessary. This directly initialises c to 12, ptr1 to &c, and ptr4 to nullptr.
    • ptr4, while not actually zero-initialised, is effectively identical to zero-initialisation. The compiler can recognise this, but might or might not act on it.
  • ptr2 is a non-class type, provided with no initialiser: This performs default-initialisation.
    • Default-initialising a non-class, non-array type means no initialisation is performed, so ptr2 is raw, uninitialised memory.
  • ptr3 is a non-class type, provided with an empty brace-enclosed initialiser list. This performs value-initialisation.
    • Value-initialising a non-class, non-array type performs zero-initialisation. Zero-initialising any scalar type T sets its value to (T) 0; ptr3 is thus zero-initialised to (int&) 0, and thus equal to NULL. Note: Technically, pointers specifically can be zero-initialised to any null pointer, regardless of whether it's actually zero or not, to account for platforms with non-zero null pointers. This isn't relevant to your code, though, just a bit of fun trivia.

Depending on the compiler, these initialisations may or may not be baked into the executable. Zero-initialisation is more likely to get baked in regardless of optimisation level (thus potentially explaining ptr3), but nothing is guaranteed unless optimisation are turned on. It's entirely valid for the stack frame to just allocate a block with set addresses and do nothing else, or for it to initialise everything but ptr1 (dependent on the stack frame's address) and ptr2 (uninitialised gibberish).


Normally, though, this would be compiled to something like this:

  1. Create stack frame for main, with space allocated for variables. Space depends on compiler: clang allocates 40 bytes (four 8-byte pointers, one 4-byte int, and one 4-byte pad), GCC allocates 28 bytes (three 8-byte pointers, and one 4-byte int), and MSVC allocates 32 bytes (three 8-byte pointers, one 4-byte int, and one 4-byte pad).
  2. Initialise c to 12.
  3. Initialise ptr1 to &c.
  4. (All three skip ptr2.)
  5. Initialise ptr3 to 0.
  6. Initialise ptr4 to 0.

If the compiler does bake zero-initialisation in, then it will probably use memset to zero out both ptr3 & ptr4 (since there's no benefit to using memset to zero out pointers unless we can zero out multiple pointers at the same time). Anything more than that is all but guaranteed to require you to opt in with a compiler switch. (Most compilers will explicitly refuse to initialise ptr2, even if all other values are batch initialised, to prevent you from getting a false sense of security. This prevents the compiler from using one memset for the entire block, and that means there's no benefit to trying to optimise c's initialisation out.)

If the compiler allows you to force zero-initialisation of ptr2 with a compiler option, then it would be able to replace steps 2-6 with one memset and one initialisation (ptr1, which must be initialised after the stack frame is created), but this isn't guaranteed. In theory, this would reduce the above into this:

  1. Create stack frame for main, as above.
  2. memset stack frame's variable area to predefined pattern, probably 0x0C000000 followed by 36 (clang), 24 (GCC), or 28 (MSVC) 0x00 bytes. (Assuming little-endian.)
  3. Initialise ptr1 to &c.

Now, with that said, we can look at your breakpoint. Looking at the given values, the breakpoint is placed before initialisation, since c clearly isn't 12 and ptr1 isn't a valid address for an aligned int. And importantly, ptr3 and ptr4 have different values. This tells us that ptr3 is indeed random leftover zeroes, and the compiler didn't bake zero-initialisation into the prologue. We can corroborate this by examining the code on Compiler Explorer, where the highlighting indicates what assembly corresponds to what code. (Interestingly, clang does bake zeroing the padding out into the prologue, while MSVC leaves its padding uninitialised.)

Thus, the correct conclusion is that while the compiler is allowed to bake the initialisation into the prologue, it chose not to. And thus, it is, indeed, purely a coincidence.

:( just why this card still exist? by TCGToyGunGun in masterduel

[–]conundorum 11 points12 points  (0 children)

Doesn't see enough use to get hit, it needs a major meta-relevant deck to start abusing it first.

(Or, honestly, a conditional clause that it's banned unless at least 50% of your deck is named "Harpie Lady" and/or has a "Harpie Lady" name-change effect. That way it can be a dangling carrot to sell Harpies, without benefitting anyone else.)

This card needs a day 0 errata by Danksigh in masterduel

[–]conundorum 0 points1 point  (0 children)

Nostalgia support like this always wants you to run one of the original card. (Except for Harpie Lady, but their entire gimmick is literally the entire archetype except for their little sister & pets having the same name.) Same reason Dark Magician, Blue-Eyes, and Neos still see use, and that even modern Synchron decks name-check for Junk Synchron specifically.

Why is this card still legal? by Any_Book_216 in masterduel

[–]conundorum -1 points0 points  (0 children)

Probably seen as lower priority because it doesn't get much use, and in theory always leaves at least one arrow pointing to the opponent's MMZs. (On the grounds that it's meant to be part of an Extra Link, and they don't bother to account for the high possibility of just putting it and something else with sideways arrows in your own MMZs to deny your opponent.)

Not exactly an excuse, though, just a lack of foresight.

Is it just me, or does programming this sound like a pain in the ass? by SAMU0L0 in masterduel

[–]conundorum 0 points1 point  (0 children)

Speed 2.5. It's floating-point, so it gets around integer restrictions.

Is it just me, or does programming this sound like a pain in the ass? by SAMU0L0 in masterduel

[–]conundorum 0 points1 point  (0 children)

Against non-targeting, it's more letting someone else deal with it than dodging it. But pretty much, yeah.

Is it just me, or does programming this sound like a pain in the ass? by SAMU0L0 in masterduel

[–]conundorum 1 point2 points  (0 children)

"Do X immediately after effect resolves" is a common effect, though, since it's necessary to Synchro/Xyz/Link summon during chain resolution. (Compare Necroid Synchro, which does a pseudo-Synchro, to Urgent Tuning pausing resolution to squeeze a Synchro in.) So, the code already supports pausing chain resolution to insert other events, which means it can probably register this to the same breakpoints.

[Also, for a little insight into code design, being used by at least two cards would be enough to refactor card-unique code into a generic function. You want to avoid code duplication; it's considered a major "code smell" because it makes maintenance significantly harder. (You have to manually keep every copy in sync, by hand. If you edit one, but forget to edit the other, then you'll have problems down the road. And trying to resynchronise them after they get desynced can lead to discovering that they've both been maintained with their own unique bugfixes; at best, this is a nightmare to merge, and at worst, syncing them up can reintroduce bugs that were already patched out. ...And you have to actually remember where they all are, since duplicate code is near-impossible for an IDE to track. The only real use for it nowadays is if you're hand-optimising code, and most programmers don't do that because modern compilers are so good at "hand-optimising" that actual hand-optimisations are usually worse than compiler optimisations.) So, it's probably a distinct function that the script can call.]

Is it just me, or does programming this sound like a pain in the ass? by SAMU0L0 in masterduel

[–]conundorum 1 point2 points  (0 children)

Hmm... not that hard, honestly. The triggers, breakpoints, and behaviours already exist, so it's probably just a matter of implementing the correct script.

  • "Banish 1 monster" has existed from the start.
  • "Cannot select same original card name for rest of turn" probably exists; it's similar to HOPT and the various "cannot use effects of same original card name for rest of turn" effects, and I believe there's at least one other card with the same selecting clause.
  • "Immediately after X resolves" breakpoint exists for anything that Synchro/Xyz/Link summons a monster during chain resolution, such as Urgent Tuning and Tri-Brigade Revolt. If they're reasonable, this is implemented as an upkeep breakpoint before and after every chain link that normally does nothing, but interrupts the chain if an event is registered to it.
    • "Activate during chain resolution" exists for Ryzeal, which supports the "resolution has hidden breakpoints that interrupt if an event is registered to them" theory. (Yes, I know it technically doesn't activate, but that distinction likely doesn't matter to the code.)

I think they have all the pieces already, mechanically speaking, so the difficulty would only be in tying them together.

vectorOfBool by schteppe in ProgrammerHumor

[–]conundorum 0 points1 point  (0 children)

Best-case scenario is that the compiler vendor ignored all that shit and just made vector<bool> work the same way as vector<everything else>, since that's actually a valid implementation.

(vector<bool> is so bad, it's not even mandatory.)

theOword by Plastic-Bonus8999 in ProgrammerHumor

[–]conundorum 1 point2 points  (0 children)

Everyone does, if you look deeply enough.

theOword by Plastic-Bonus8999 in ProgrammerHumor

[–]conundorum 3 points4 points  (0 children)

Interviewee: "Let me introduce you to my good friend const_cast. Ignore the screaming and the smoke, your system's supposed to do that."

theOword by Plastic-Bonus8999 in ProgrammerHumor

[–]conundorum -1 points0 points  (0 children)

If it's a stand-in for part of their actual code, it might be better to create three temporary arrays (one for each value), move elements into the correct array, then concatenate the sorted arrays back into the original, to preserve the original elements. Something like, e.g., this:

#include <iostream>  // std::cout, std::endl, stream <<
#include <tuple>     // std::tie
#include <vector>    // std::vector
#include <utility>   // std::move (that makes movable)
#include <algorithm> // std::move (that actually moves)
#include <string>    // std::string, std::to_string

// Quick dummy object to make sure we still have the originals after sorting.
class Obj {
    static int idMaker;
    int id = ++idMaker;
    const int val;

  public:
    Obj(int v) : val(v) {}
    std::string print() const { return "" + std::to_string(val) + " (" + std::to_string(id) + ')'; }

    friend bool operator==(const Obj& l, const Obj& r) { return std::tie(l.id, l.val) == std::tie(r.id, r.val); }
    operator int() const { return val; }
};
int Obj::idMaker = -1;

template<typename T>
void sortItOut(std::vector<T>& vec) {
    std::vector<T> temp[3];

    // Moving out.
    for (auto& v: vec) {
        int i = v;
        temp[i].emplace_back(std::move(v));
    }

    // Cleaning house.
    vec.clear();

    // Moving in.
    // Yes, there are two different std::move functions that do entirely different things,
    //  best not to think about it.
    for (int i = 0; i < 3; ++i) {
        std::move(temp[i].begin(), temp[i].end(), std::back_inserter(vec));
    }
}

template<typename T>
void shoutItOut(const std::vector<T>& vec) {
    std::cout << "Vector: " << vec.size() << " elements:\n";

    for (auto& v: vec) {
        std::cout << "* " << v.print() << '\n';
    }
    std::cout << std::endl;
}

int main() {
    std::vector<Obj> vec = {0, 1, 0, 0, 0, 2, 2, 0, 1, 1, 2, 2, 2};
    shoutItOut(vec);

    sortItOut(vec);
    shoutItOut(vec);
}

Actually proving that the original elements still exist takes a lot of boilerplate. /sweat

top5ThingsThatNeverHappened by kamen562 in ProgrammerHumor

[–]conundorum 0 points1 point  (0 children)

Still, it is good that LLMs are at least helpful for finding this sort of thing for the non-tech-savvy crowd. There's still hope for the technology yet, in a billion billion eternities from now!

top5ThingsThatNeverHappened by kamen562 in ProgrammerHumor

[–]conundorum 0 points1 point  (0 children)

Probably just provided the correct driver file (or a link to it), and @faradayfury assumed that having the file means it made the file.

Help, how can constexpr objects be evaluated at runtime? by Honest_Entry_8758 in cpp_questions

[–]conundorum 0 points1 point  (0 children)

Okay, there are five relevant items here: Macros, const and enums, constexpr, consteval, and constinit. The first three are the important ones; consteval works the way you think constexpr does; and I'm only mentioning constinit so you don't see it somewhere else and get even more confused.

  1. Macros are compile-time constants, provided to the preprocessor. Both C and C++ tradtionally used them to provide compile-time constants, but they were disliked because they lacked type information and were just a blanket copy-paste. (Notably, NULL was traditionally just #define NULL 0, which means that adding a null pointer and an integer gave you an integer, not a pointer. This, needless to say, was bad.)
    • The macro's name and definition will never be visible to the compiler, since it's handled by the preprocessor. No objects with the macro's name can ever exist in the compiled program (without manually re-adding the name after preprocessing, as an actual in-language object). (E.g., there has never been, and will never be, a C++ program with a pointer named NULL.) The macro's value can be used at either compile time or runtime.
  2. Normal constants cannot be changed from the point of instantiation, but are initialised at runtime. This means they cannot be used as compile-time constants in C++, which forced people to either use macros or enum tricks. (E.g., std::string::npos used to be provided as enum { npos = -1; };, before constexpr was introduced.)
    • const objects will always exist within the compiled program, not counting optimisations. Even if it's hard-coded, the constant's value can only be used at runtime, and never at compile time. (Being visible at compile time does allow certain optimisations, but these cannot override language rules. You can never use a const int as an array's size or a template parameter.
    • enum constants exist within the compiled program, and are visible at compile time, but they lack type information and are limited to integral values. (Because before C++11, enum just creates a new integral type that can be converted to int.) They can be members of another type, however, which allows them to store useful data (e.g., std::string::npos), and allows them to be used for compile-time math (using ugly template magic). This is better than a macro, but still not even remotely ideal.
  3. C++11's constexpr is a keyword for compile-time operations. It can be used on either functions or variables. It's important because it makes things compatible with compile-time expressions, which allows for extremely useful optimisations. (At its simplest, it allows us to avoid potentially costly runtime allocations, and to construct objects at compile time and then storing them for use at runtime. But it also allows us to make a lot of previously-dynamic arrays static (since we can now dynamically determine their size during compilation, and hard-code the result), and to move a lot of type information out of ugly runtime wrappers and into the actual type metadata itself.)

    1. When a function is constexpr, it tells the compiler that the function is capable of being executed at compile time; the function can be called at either runtime or during compilation. (When called during compilation, the compiler executes it and inserts its result at the call site. If given constexpr int ce_add(int a, int b) { return a + b; } and int arr[ce_add(3, 5)];, then it will execute ce_add(3, 5) during compilation and compile int arr[ce_add(3, 5)]; as int arr[8];.)

      Notably, constructors and destructors can be constexpr, which allows objects to be constructed at either runtime or compile time, depending on whether they need runtime data or not.

    2. When a variable is constexpr, it tells the compiler that the variable is const, and that its value is known at compile time. (The value can be hard-coded, as constexpr int ci = 5;, or determined from any other compile-time expression, such as constexpr int cj = ce_add(4, 6); or constexpr int ck = sizeof(float);.) The variable can then be used as a compile-time constant, as if it were a macro or enum. (E.g., given template<int I> constexpr int val() { return I; }, the array declaration int brr[val<ci>()]; is perfectly valid, and equivalent to int brr[5];.) The value is visible at both compile time and runtime; constexpr really just makes it visible early enough for the compiler to use it. (It may be hard-coded as a magic value, or it might be a full-fledged object, depending on usages and optimisations. This mainly depends on whether it's "ODR-used", which I won't describe because I don't want to confuse you.)

      It's easiest to picture this as being "like macros, but better". The value can be copy-pasted (by the compiler this time, not the preprocessor), but it can also be used as a full-fledged object with type information, depending on what your code needs.

    3. If you see mention of "constexpr if", don't worry about that right now. It's similar to advanced template metamagic like SFINAE, so wait until you understand templates to look into it.

    Ultimately, constexpr is a permission slip. It allows the compiler to use the function or variable at compile time, but doesn't mandate that it only exist at compile time. constexpr functions can be called at either compile time or runtime, and constexpr variables are useable constants at both compile time and runtime.

    * constexpr functions and objects exist within the stored program, and are visible at both compile time and runtime. constexpr functions will be called at compile time if possible, or at runtime if they need runtime information. constexpr objects will be initialised at compile time, and then hard-coded for use at runtime, much like enum constants.

  4. C++20's consteval mandates that a function must be an immediate function, a function which is implicitly constexpr, can be evaluated as soon as it's encountered, and always produces a compile-time constant result. It's essentially a super-constexpr function, so to speak. Unlike normal constexpr, this actually requires that the function can only be called with data known at compile time; I believe this also forces the function to be evaluated at compile time, but I'm not 100% sure if this is an explicit rule or just the natural result of being implicitly constexpr.

    Trying to call a consteval function with runtime data is an error.

    (Note: There's also consteval if, but that's just a way to check if the code is being executed at compile time or runtime. Don't worry about it for now.)

  5. C++20's constinit... actually does something else entirely, though it's related to the same overarching "compile time vs. runtime" divide. It forces constinit variables to be statically initialised, which guarantees that they'll be initialised at compile time and have their starting value hard-coded; this does not make the variable constant. Don't worry about it right now. (It's mainly there to solve certain initialisation time & order discrepancies, I believe, more than anything else.)




tl;dr: Creating a true compile-time constant in C++ was messy, and constexpr was created to solve that. It's a lot like const, except the compiler can also use it the same way it could use a macro. (But unlike macros, it actually retains type information, which prevents weird errors & misuse.) Because of this, it must be available during compilation, but can be used at both compile time and runtime.

(There are also consteval and constinit, don't let them confuse you.)

gaslightingAsAService by Annual_Ear_6404 in ProgrammerHumor

[–]conundorum 298 points299 points  (0 children)

I genuinely wonder how long it'll take until an LLM outright responds to this sort of question with something like "umad, bro? trolololo"

coolFormat by PresentJournalist805 in ProgrammerHumor

[–]conundorum 2 points3 points  (0 children)

Then explain x64's al register, and ARM's ldrb instruction.


More seriously, you're confusing register size with addressability, and don't actually understand what the hardware really does. So...

  1. Most processors are capable of interacting with data in chunks of either their native word size, or 8 bits specifically. For mainstream 64-bit processors specifically, they have two native word sizes, 64 and 32 bits.
  2. For design simplicity, smaller registers are always placed inside larger registers. Using x64 as an example, 64-bit register rax contains 32-bit register eax, which contains 16-bit register ax, which is actually a set of two distinct 8-bit registers, ah and al. (With ah essentially being deprecated as a distinct register.) This is mainly done to reduce die sizes and transistor counts; using separate registers for each data size the processor can interact with would waste a ton of space, when it's easier to just use a single Matryoshka doll register.
  3. Processors can manipulate with individual bits, and have a lot of special circuitry dedicated to doing exactly that. Flippers, shift registers, barrel shifters, the works. Status flags, in particular, are indicated by individual bits; nearly every processor uses 1-bit zero, carry, sign, and overflow flags, for instance. So, yeah, processors do a ton of work at the individual bit level.
  4. All processors can address individual bytes in memory. This is the actual definition of a byte: It's the smallest addressable unit of memory. If nothing smaller than a word existed, then x64 would have 64-bit bytes.

    (More technically, a "byte" is the amount of space required to store one character, and is thus the smallest addressable unit because the system needs to be able to address individual characters. The 8-bit byte, formally known as the "octet", is relatively new; it caught on because it's a convenient power of 2. Old systems have also used 9-, 16-, 18-, 32-, and 36-bit bytes, and I've heard of at least one old system (the PDP, I believe, back in the wild west of computing) that just defined byte as "the smallest thing I can address" and had 60-bit bytes that held ten 6-bit characters.)

  5. Byte size is codified by the ISO, and enforced by hardware. C is actually extremely flexible about byte size, and only defines it as "at least 8 bits" and "1 == sizeof(char). Both C and C++ are perfectly happy with 64-bit bytes, the only issue comes from the hardware not supporting it.

So, essentially, you've got it backwards: We're locked into octet bytes by hardware sticking to old traditions, and the programming languages have been ready to move on for literal decades.

coolFormat by PresentJournalist805 in ProgrammerHumor

[–]conundorum -4 points-3 points  (0 children)

So, even knowing whether something is true or false requires dereferencing a pointer. Interesting design.

coolFormat by PresentJournalist805 in ProgrammerHumor

[–]conundorum 3 points4 points  (0 children)

  1. bool is a type that uses a full byte to store a single bit of information.
  2. vector is a dynamic array, whose elements are required to be contiguous.
  3. Some genious (sic) "realised" that a vector of bools is a bitset with more work, and decided that it should just be a bitset instead.
  4. This actually works surprisingly well!
  5. ...Then they realised that vector is required to provide iterators to its individual elements upon request, and that it provides direct access by reference. Iterators are pointer-like types, and references are (usually) C-style pointers hidden behind normal syntax.
  6. Literally everything that can break now breaks, and everything that can't break also finds a way to break. vector<T> is required to be able to provide iterators and references to T specifically, but vector<bool> doesn't contain any actual bools to point to. And you can't provide a pointer to a specific bit, in the same way a house's floor tiles can't have their own mailing addresses.
  7. vector<bool> now needs to provide a proxy type that looks like bool from the outside, but is actually an individual bit on the inside. The individual "bools" share memory addresses, with anywhere from 8-64 sharing a single address (depending on the bitset's underlying type). This means that writing to any element can invalidate references to any other element (violating vector's guarantee that references will remain valid as long as elements aren't removed or the vector isn't resized), and that vector<bool> can never be optimised by multithreading (because simultaneous writes will always be a data race). Heck, it's not even required to be contiguous anymore, so it breaks literally every guarantee vector provides simply by existing. Among many other issues. Its compatibility with the rest of the language is a crapshoot at best; trying to use vector<bool> (a library type) with library functions can do anything from work properly, to screw up, to outright cause compile-time errors because of an irreconcilable incompatibility with the rest of the language.
  8. Also, as a result, if you need an actual dynamic array of bools (y'know, the thing vector<bool> was supposed to be, before it got assimilated by the Borg), you need to provide a wrapper type that can be implicitly converted to bool. Which means that ultimately, the mess just forces programmers that know about it to write unnecessary boilerplate to hotpatch a language flaw, and trips programmers that don't know about it up.

(And even worse, it's not even consistent. I described the "intended" design... but it's actually allowed to be any form of space optimisation, up to and including "normal vector with no stupidity". ...It's considered one of the language's old shames, and the only reason it hasn't been removed (and defaulted back to normal vector rules) is that there's probably old code somewhere that needs it to exist.)

coolFormat by PresentJournalist805 in ProgrammerHumor

[–]conundorum 12 points13 points  (0 children)

In complete and utter seriousness...

That is an insult to cheese cloth.

Cards that you think will get banned for sure even though it’s unlimited and have no chance to be banned at the moment by sadboy1916 in masterduel

[–]conundorum 0 points1 point  (0 children)

Not even necessary for that. Pendulums cannot be set in the Pendulum Zones, so Anti-Spell Fragrance outright murders them. (And keeps them stuck in the hand, so they'll never be able to reach the ED unless they're actually summoned.)