Quote:Original post by antiquechrono
I'm not sure if you are doing quantum computing or something, but last I checked the most basic functionality of a processor is to fetch instructions from an address and execute them and in the process more than likely manipulate data which is at another address. It has no concept of a variable, just an addresses to data. Now call me old fashioned but that sounds an awful lot like what a pointer is.
Wrong, on both counts.
First, your description is a gross oversimplification of what the processor does. If this is the kind of 'machine-level' knowledge that can be gathered from using C, I don't really see why we should bother.
Consider a simple C statement such as z[1337]++;. To the C programmer, the program will extract the value at the 1337th position of the buffer pointed to by the integer pointer z, add 1 to that value, and store it back at that position. So far, so good. This is not what happens at the machine level.
First, there's the processor. Today, it's almost guaranteed to be a multi-core one, or perhaps a hyper-threading one. Thus, there's the initial question of which processor the code will be executed on, something which C has nothing to say about.
Then, there's the pipeline. Your typical processor today has between 9 and 30 pipeline stages, which are yet again completely unknown to the C language. Pipeline stages include fetching the instruction, decoding it, fetching memory, processing, waiting, retrieving, outputting, and so on. This is a fundamental point in optimization, because a full pipeline stall in a 30-stage pipeline will divide your performance by 30. In our situation here, using either z or the contents of the buffer nearby the write point will result in read-before-write conflicts and will stall the pipeline for the length of the increment processing.
Then, there's the instructions. Instead of being loaded from disk (where the applications reside), code is first-stage cached in memory and second-stage cached in an on-chip instruction cache. The entire caching process is completely invisible to the C application, which merely sees function pointers and goto labels, yet it is important because of the latency induced by potential cache misses on long jumps.
Then, there's the memory access. Your pointer access may have been successfully aliased by the compiler to a single register representing z[1337], which will need no memory addressing. Or you might be working with register sets in SIMD style, too. Otherwise, chances are that you'll hit the L1 cache, though it is doubtful as the buffer pointed to by z is bigger than many L1 caches. Thus, you might have to fetch the data from the L2 cache. Even assuming that the data was either in the L1 cache or the L2 cache, there's still the possibility that another core on your processor has altered the data, which means that a synchronization protocol between cores might be activated to fetch the data of the other core, just in case. Plus, once the data has been fetched, it might be automatically realigned in case it wasn't in the first place. The C language pointers do not even begin to hint at the depth of all this. And the memory address has not even been decoded yet!
Because, then, you get the actual, uncached, memory access. Your pointer will quite probably be converted, in any recent operating system, to segmented memory (an old relic from the early x86 architectures). The point is that the memory space for every process on your computer is guaranteed to be deterministic and linear by the operating system. That is, each address your program manipulates is an integer number from a minimum value to a maximum value (some of which have not been allocated yet). What happens is that the operating system maps your linear addresses to segments (or, if you prefer, pages) which are handled non-linearly (you have a page index, and then the offset within that page). When you access an address, your processor automatically converts the address based on the instructions it was given by the processor.
If you hit an allocated page that your process is allowed to manipulate, the processor will send read or write instructions through the memory bus, with all the implied latency of such reads. Cache policies will intervene here to determine which L1 and L2 cache lines get filled and which don't, in order to optimize sequential memory access.
If you hit an unallocated page, the operating system is notified through a processor interrupt and will deal with the issue accordingly, usually terminating the infringing process for an access violation or segmentation fault (the terminology depends on the OS culture). This involves suspending your program, flushing the pipeline without damaging too many things, loading the kernel code into the instruction cache for that interrupt handler, elevating to a lower ring, and resuming execution within the kernel's handler. This gets even uglier with multi-cores.
If you hit a guard page, the operating system is also notified, but the reaction will vary. If the page is a lazy-allocation page (that is, you asked for 1GB memory, so the OS gave you 1GB worth of pages but not the memory corresponding to those pages, and will only give you the memory for one of the pages if you actually ask for it), then the operating system interrupt handler will fetch and reserve actual memory, bind it to the segment at the processor level, and resume interaction. Other pages include memory-mapped files or devices, at which point the operating system will forward the written data to the appropriate device driver as part of the interrupt—the actual details of how flushing, caching, multi-cores, pipelines and the rest actually interact with this are too horrible to mention here.
Yet, there's not even the slightest hint in the C language about the existence of pages. Which is not surprising, since C might also be used on page-less, cache-less or pipeline-less architectures. In the end, the result of z[1337]++; is an agonizing detailed and complex process that goes
way beyond simply reading data to and from memory. And almost none of it can be inferred from the C operational semantics.
Second, your interpretation of pointer semantics is also oversimplified, and a bit off. This is due, usually, to books and tutorials which give a simplified idea of what pointers are ("memory addresses" is a very frequent misleading explanation, though it does get the fundamental points across) without mentioning that the concept is actually much more complex.
A pointer-to-X rvalue, where X is an actual first-class type, can be one of three distinct things:
1 the 'null pointer' for the type X, which represents the absence of any value. The null pointer evaluates to false in a boolean context (while all other pointers evaluate to true), and the integer constant zero evaluates to the null pointer in a pointer context.
2 an lvalue of the type X. This is the usual 'points at an object of type X'. The definition of lvalue says everything there is to know here. The lvalue and its corresponding rvalue can be accessed through dereferencing (*ptr).
3 a past-the-end pointer. Unlike the null pointer, past-the-end pointers are many, and they differ from each other through '==' comparison. They cannot be dereferenced.
Then, there's the grouping of lvalues: they are grouped in buffers containing zero or more lvalues. A pointer to an lvalue can be incremented or decremented, changing its rvalue the previous or next lvalue in the buffer if it exists, otherwise resulting in either that buffer's past-the-end pointer (if incrementing) or in undefined behaviour (if decrementing). Decrementing a past-the-end pointer yields the last lvalue in the associated buffer, or undefined behaviour if the buffer is empty. Such buffers are created every time you allocate data on the stack or heap, with pointers to the first lvalue being returned in the latter case, or obtained with &var in the former.
The matters are further complexified by the notion of memory layout compatibility, which allows one to see a buffer of X lvalues as a buffer of Y lvalues, under certain conditions of alignment, padding and size. These, I will not go into here, but they are the fundamental element behind casting structures to a buffer of bytes, or behind unions.
The usual 'pointers are addresses' works fine, as long as you consider an address to be a synonym for an lvalue or past-the-end, though it does miss on a lot of subtleties described above. And as soon as you get the strange notion that addresses are numbers, which is almost universally inflicted upon beginners by tutorials and books, you're off course. Unlike numbers, pointers can only be compared for order in very specific cases: when they're within the same buffer. Unlike numbers, pointers cannot reliably be converted to and from numbers (though C99 has done some efforts to solve this) and can certainly not respond correctly to arithmetics on numbers. The list of discrepancies goes on. Ultimately, code such as z[1337]++; actually consists in incrementing an lvalue, not accessing a memory address and incrementing the value found there.
Morality for beginners: the C language is its own complex world that's quite different from how the machine actually works. Knowledge of C will more often than not confuse you about how the machine works, instead of granting you knowledge about it.
Quote:This is an interesting idea, but how come I never see any smart pointers used in acctual code or examples? My professors certainly have never even mentioned smart pointers before, do you have any resources I can look at discussing this because I have just been taught that *ptr is the way to go.
Most teachers are not competent enough to be in the industry working a well-paid C++ job. This is generally why they're teachers instead. There are, of course, exceptions which teach out of pleasure, not for money.
All C++ job interviews I've passed have asked for or even checked through tests my knowledge of the SC++L and sometimes even the boost library. And France isn't quite known for its IT prowess.