Tuesday, June 28, 2022

Digest for comp.lang.c++@googlegroups.com - 25 updates in 4 topics

Juha Nieminen <nospam@thanks.invalid>: Jun 28 01:56PM

One of the strangest and perhaps most surprising (and even controversial)
new features of C++20 are coroutines. This post is not about bashing them,
criticizing them, questioning the motivation for adding them to the
already complex language, or anything like that. Let's put all that aside
for a moment, and just focus on what they can actually be useful for.
Practical examples.
 
I had never heard of "coroutines" before, and in fact it took me quite
a while to understand what exactly they are, and what they are used for.
They are often described as kind of emulating cooperative multitasking
(similarly to how multitasking worked in Windows 3.x and MacOS prior
to X.)
 
I would, however (and from what I understand), describe them more like a
way to more easily store the state of a function (or group of functions)
so that the function can be exited at any point that it wants, and then
execution resumed later from that point (when the rest of the code
tells it to resume execution.)
 
(Well, I suppose that's exactly what "cooperative multitasking" means,
but I think describing it as I did is much clearer than using that term.)
 
Of course doing this was already possible even before C++20, but it
required a lot more manual labor, a lot more coding. The coroutines
help automatize the process and make it much easier.
 
So where is this actually useful? I can think of two examples.
Are there more?
 
Example 1:
 
You are writing a function that decompresses some compressed input format.
The decompressed data should be written to a buffer of a given size.
Whenever the buffer gets full, this decompressor function should return
it to the calling code for it to consume it. After the calling code has
consumed it, the decompression should continue from where it left.
Since compression formats are complicated there may be literally dozens
of places in the decompression algorithm where the buffer may get full,
in very varied situations, sometimes even inside deeply nested loops,
coroutines make this process a lot easier because the code can "yield"
at any point, anywhere (even inside deeply nested loops), and then
continue from that point as if nothing has happened (other than the
buffer having been emptied).
 
Example 2:
 
A class/module that parses command-line arguments. Since the arguments may
appear in many different forms, the parser is done so that it will be
called with individual command-line parameters (which may end in the null
character, or a whitespace character, or something else). In other words
for each individual parameter the calling code calls the parser giving it
that parameter.
 
However, not all command-line parameters are singular, as in consisting
of only one "word". A parameter may be followed by one or more strings
(such as file names, etc.) The calling code doesn't know if a particular
parameter is just data for the previous parameter or not, it just feeds
them one by one to the parser.
 
Thus, when the parser encounters a parameter that expects further
parameters, it needs to remember its state between calls. (It needs to
remember which particular command-line parameter it's currently
parsing, and any additional parameters given to it already.)
(This, of course, can be done even with C++98, but with coroutines this
"storing the current state of the parser" becomes much easier.)
Bonita Montero <Bonita.Montero@gmail.com>: Jun 28 04:47PM +0200

If you don't like coroutines, don't use them.
Ben Bacarisse <ben.usenet@bsb.me.uk>: Jun 28 01:14AM +0100

>> verb), and I don't think it does in mathematics either. And in C and
>> C++, incrementing means adding one.
 
> Not really.
 
It really does. It's right there in the definition.
 
> to the same machine code. But that machine code will almost certainly
> be "add 4 to the address held in ptr". In fact what you mean is "go to
> the next value in the sequence".
 
What happens to the representation of the pointer when we add one to it
should not prevent you from believing that you are adding 1. You are
adding 1.
 
> resolution of 1mg. However the fact we are using metric units is
> merely conventional. It has no scientific significance. As is our
> choice of 1mg as the spacing between the tests. So it's "accidental".
 
Ah, scare quotes. I agree it's "accidental" but not that it's
accidental.
 
> on, up to ten men, digging a hole, and time them. Now the choice of
> one is not "accidental". It's fundamental that we can only have a
> whole number of workers.
 
But you said
 
"Adding one means "increase by a constant value which accidentally
happens to be unity".
 
I see neither an accident nor an "accident" here. Adding one means
increasing by unity. That seems to me to be fundamental.
 
>> "increment by".
 
> Exactly. ++ should mean "take the next member of an ordered set". Which it
> does in C++.
 
Eh? It means add one in C++: "The value of the operand object is
modified (3.1) by adding 1 to it". What is the next member after 1.0 of
the ordered set of double values? What is ++x when x is 1.0?
 
> In C++ "++" can't always be replaced by "+=1". In C it can.
 
Eh (again)? Both standards give that equivalence so as to delegate all
the details about types and conversion to the descriptions of addition
and assignment.
 
 
--
Ben.
Malcolm McLean <malcolm.arthur.mclean@gmail.com>: Jun 27 06:42PM -0700

On Tuesday, 28 June 2022 at 00:02:50 UTC+1, Keith Thompson wrote:
> > in the sequence".
> Which you do by adding 1. In pointer arithmetic, adding 1 has a well
> defined meaning, which is not advancing to the next memory address.
 
In pointer arithmetic, adding one does not mean adding one to the address.
So it's "adding one", but only if, in this context, we accept that "adding one"
means "add four" (or however many bytes an integer occupies).
ptr++ means "go to the next integer in the sequence and take its address".
 
You can of course also say it's "adding one" because of C's rules for
pointer arithmetic. But it's not "really adding one".
Take this.
int x;
int* iptr = &x;
unsigned char* cptr = iptr;
if (iptr == cptr)
printf("pointers the same\n");
iptr++;
cptr++;
if (iptr == cptr)
printf("pointers the same\n");
 
Now iptr and cptr are equal, we add the same value to them and then they are not
equal. This isn't "wrong" because we're not working with the normal rules for
arithmetic. But we're using the word "add" in a special way. So we can say "we're
not really adding one", using the normal meaning of the word.
Malcolm McLean <malcolm.arthur.mclean@gmail.com>: Jun 27 06:52PM -0700

On Tuesday, 28 June 2022 at 01:15:14 UTC+1, Ben Bacarisse wrote:
> happens to be unity".
> I see neither an accident nor an "accident" here. Adding one means
> increasing by unity. That seems to me to be fundamental.
 
If we've got two workers and add another, we have increased by unity.
If we've got two grams of sugar and we add another gram, we've done
something rather different.
The symbol "1" appears in both cases if you write it out. But it doesn't
mean the same thing. In one case it expresses an underlying, fundamental
property of the thing it represents. In the other case it doesn't. It expresses
an accidental property of the sugar.
Keith Thompson <Keith.S.Thompson+u@gmail.com>: Jun 27 07:27PM -0700

>> Which you do by adding 1. In pointer arithmetic, adding 1 has a well
>> defined meaning, which is not advancing to the next memory address.
 
> In pointer arithmetic, adding one does not mean adding one to the address.
 
Yes, it does -- because that's how C and C++ define the operation of
adding a pointer and an integer.
 
> So it's "adding one", but only if, in this context, we accept that "adding one"
> means "add four" (or however many bytes an integer occupies).
 
No, adding an integer value N to a pointer value does not mean adding N
bytes (unless sizeof *ptr == 1).
 
> ptr++ means "go to the next integer in the sequence and take its address".
 
Given that ptr is of type int*, yes, more or less. (I'd phrase it
differently.)
 
ptr = ptr + 1 means exactly the same thing.
 
> You can of course also say it's "adding one" because of C's rules for
> pointer arithmetic. But it's not "really adding one".
 
Yes, it's really adding one.
 
> equal. This isn't "wrong" because we're not working with the normal rules for
> arithmetic. But we're using the word "add" in a special way. So we can say "we're
> not really adding one", using the normal meaning of the word.
 
We're using "add" in the way defined by the language. That is the
normal meaning.
 
Please stop trying to confuse the issue by applying other meanings to
terms that are already well defined.
 
Yes, we know that some of these terms can have different meanings in
other contexts. You're insisting that those other meanings are somehow
more "normal" than the meanings defined by the language. That's silly.
 
--
Keith Thompson (The_Other_Keith) Keith.S.Thompson+u@gmail.com
Working, but not speaking, for Philips
void Void(void) { Void(); } /* The recursive call of the void */
Malcolm McLean <malcolm.arthur.mclean@gmail.com>: Jun 27 08:37PM -0700

On Tuesday, 28 June 2022 at 03:28:10 UTC+1, Keith Thompson wrote:
 
> Yes, we know that some of these terms can have different meanings in
> other contexts. You're insisting that those other meanings are somehow
> more "normal" than the meanings defined by the language. That's silly.
 
It would have been more consistent for C to have defined
 
ptr += sizeof(*ptr);
 
as the way of moving a pointer along to the next position.
However it's such a common requirement that this wasn't done, and instead
the concept of "scaled arithmetic" was developed.
When you change the rules of arithmetic like this, inevitably you break some
of the relationships that hold in arithmetic over the set of the real numbers.
In this case
a = x;
b = x;
therefore a + c = b + c;
no longer necessarily holds.
So "a + c" means something different to what is normally understood by
these symbols. What is meant is sufficiently close to the operation of addition
over the real numbers to make it reasonable to use the same word, as
long as you make clear that you are using the word :add" in a special
way.
Keith Thompson <Keith.S.Thompson+u@gmail.com>: Jun 27 11:07PM -0700


> It would have been more consistent for C to have defined
 
> ptr += sizeof(*ptr);
 
> as the way of moving a pointer along to the next position.
 
No, it really wouldn't. Pointers are not numbers. It's useful to
define a few limited arithmetic operators on them:
pointer + integer -> pointer
pointer - integer -> pointer
pointer - pointer -> integer
 
Defining ptr+1 to yield a useless and invalid pointer would have been
silly.
 
Do you expect pointer+pointer or integer-pointer to have some consistent
meaning?
 
> b = x;
> therefore a + c = b + c;
> no longer necessarily holds.
 
Sure it does, as long as the types are consistent. Adding 1 to an int*
pointer is a *different operation* than adding 1 to a char*.
 
> over the real numbers to make it reasonable to use the same word, as
> long as you make clear that you are using the word :add" in a special
> way.
 
It's a lot easier to understand pointer arithmitec the way it's actually
defined than to start with some inconsistent mental model and adjust it
to fit reality.
 
--
Keith Thompson (The_Other_Keith) Keith.S.Thompson+u@gmail.com
Working, but not speaking, for Philips
void Void(void) { Void(); } /* The recursive call of the void */
Juha Nieminen <nospam@thanks.invalid>: Jun 28 06:40AM

> pointer - pointer -> integer
 
> Defining ptr+1 to yield a useless and invalid pointer would have been
> silly.
 
One can think of "ptr+1" as being just syntactic sugar for "&ptr[1]".
(Technically it might not be "just syntactic sugar", but one can
think of it as such.)
Malcolm McLean <malcolm.arthur.mclean@gmail.com>: Jun 28 02:32AM -0700

On Tuesday, 28 June 2022 at 07:41:06 UTC+1, Juha Nieminen wrote:
> One can think of "ptr+1" as being just syntactic sugar for "&ptr[1]".
> (Technically it might not be "just syntactic sugar", but one can
> think of it as such.)
 
That's another reason for scaled pointer arithmetic. So that you can convert
between ptr[2] and *(ptr + 2).
I'm not saying that the decision to use scaled pointer arithmetic was wrong.
 
I am saying that ptr += 1 is not "really" adding one to the pointer, if it is not to
a single byte type. "Really" you are adding the size of the item the pointer points
to.
You can say "a pointer is not a number", and that's true in the important exceptional
case of old x86 segment::offset pointers. But these never really worked well in
C in any case. Compilers introduced the "far" keyword to help deal with them.
 
My sister's address is 15. But in fact she lives in the thirteenth house in the street. All
rules for numbers don't necessarily apply for house address numbers. But they
are still called numbers.
"Öö Tiib" <ootiib@hot.ee>: Jun 28 03:05AM -0700

On Tuesday, 28 June 2022 at 12:32:34 UTC+3, Malcolm McLean wrote:
> > think of it as such.)
 
> That's another reason for scaled pointer arithmetic. So that you can convert
> between ptr[2] and *(ptr + 2).
 
It is other way around. The operator [] is defined in terms of pointer arithmetic
so 2[ptr] is *(2 + ptr) and so required to work. We can't have circular definitions
in specification.
 
 
> I am saying that ptr += 1 is not "really" adding one to the pointer, if it is not to
> a single byte type. "Really" you are adding the size of the item the pointer points
> to.
 
IOW you see yourself that making it in any other way would be silly. Pointer
is internally address but that is not exposed in its interface.

> You can say "a pointer is not a number", and that's true in the important exceptional
> case of old x86 segment::offset pointers. But these never really worked well in
> C in any case. Compilers introduced the "far" keyword to help deal with them.
 
You need to use reinterpret_cast to get the integer value of that address out
of pointer, or to turn it into byte address ... and then you need to know what
platform you are at and what you are doing.
Malcolm McLean <malcolm.arthur.mclean@gmail.com>: Jun 28 03:15AM -0700

On Tuesday, 28 June 2022 at 11:05:51 UTC+1, Öö Tiib wrote:
> It is other way around. The operator [] is defined in terms of pointer arithmetic
> so 2[ptr] is *(2 + ptr) and so required to work. We can't have circular definitions
> in specification.
 
I don't know exactly what Denis Ritchie was thinking.
But I suspect he decided early on that [] would be the subscript operator, and that
incrementing the subscript by one would move to the next item in the array.
The last was a no-brainer.
 
Then you've got the question of how ptr[i] relates to *(ptr + i). Did he decide that
arrays would be 0-based, then decide that pointer arithmetic would be scaled?
Or did he decide that *(ptr + i) would mean ptr[i], and then accept, maybe reluctantly,
that that entailed 0-based arrays? I don't know.
David Brown <david.brown@hesbynett.no>: Jun 28 03:07PM +0200

On 28/06/2022 11:32, Malcolm McLean wrote:
> You can say "a pointer is not a number", and that's true in the important exceptional
> case of old x86 segment::offset pointers. But these never really worked well in
> C in any case. Compilers introduced the "far" keyword to help deal with them.
 
You are wrong. A pointer is /not/ a number. It is /not/ an address.
If you find that confusing, then it is no wonder you are writing such a
stream of muddled posts.
 
As a general definition, a "pointer" is a way to access data indirectly.
 
In C, it is basically "an address with type information". (I say
basically, because it can also represent information about what may or
may not be accessible as fully defined behaviour, and of course weird
implementations can have other data in a pointer.) In C++, that applies
to "raw" pointers - other kinds of pointers may have extra information.
 
If "p" is a pointer, p++ adds 1 to the /pointer/. It does not
necessarily add 1 to the address stored by the pointer - we very rarely
care what happens under the hood.
 
 
If "p" and "q" are two pointers, and "n" is an integer, then it is
always the case in C that "p + n == q + n", as you would expect. That
is because the expression makes no sense in the language if "p" and "q"
point to incompatible types, and the compiler should reject the code
with an error if they are not compatible.
 
 
It is important to understand the difference between what a high-level
concept like "pointer" represents, and how it might happen to be
implemented in a particular case - even if the implementation is obvious
and close to universal.
David Brown <david.brown@hesbynett.no>: Jun 28 03:17PM +0200

On 27/06/2022 23:32, Malcolm McLean wrote:
>> verb), and I don't think it does in mathematics either. And in C and
>> C++, incrementing means adding one.
 
> Not really.
 
As Ben and Keith have said, it is in the definition in the C standards.
(In C++, is always possible to have different effects on operators of
your own types - but the same definition applies in C++ for the built-in
integer, floating point and raw pointer types.)
 
 
To quote from C11 :
 
(6.5.2.4)
"""
The result of the postfix ++ operator is the value of the operand. As a
side effect, the value of the operand object is incremented (that is,
the value 1 of the appropriate type is added to it). See the discussions
of additive operators and compound assignment for information on
constraints, types, and conversions and the effects of operations on
pointers.
 
The postfix -- operator is analogous to the postfix ++ operator, except
that the value of the operand is decremented (that is, the value 1 of
the appropriate type is subtracted from it).
"""
 
(6.5.3.1)
"""
The value of the operand of the prefix ++ operator is incremented. The
result is the new value of the operand after incrementation. The
expression ++E is equivalent to (E+=1).
 
See the discussions of additive operators and compound assignment for
information on constraints, types, side effects, and conversions and the
effects of operations on pointers.
 
The prefix -- operator is analogous to the prefix ++ operator, except
that the value of the operand is decremented.
"""
Ben Bacarisse <ben.usenet@bsb.me.uk>: Jun 28 02:55PM +0100


> If we've got two workers and add another, we have increased by unity.
> If we've got two grams of sugar and we add another gram, we've done
> something rather different.
 
You can't do either in C++. In C++ you manipulate numerical quantities,
and unity is 1 when talking about numerical values. In C++ I'd write
 
workers += 1;
grams_of_sugar += 1;
 
I have added 1 in both cases and there is nothing accidental about
that. It was done (presumably) because that is what the program design
needs at this point.
 
> doesn't mean the same thing. In one case it expresses an underlying,
> fundamental property of the thing it represents. In the other case it
> doesn't. It expresses an accidental property of the sugar.
 
I know what you are getting at, but in C++ adding unity means adding
one. The fact that we designed the program so that adding one was the
right thing to do at that point was not an accident though they may have
been some choice in the matter. The fact that some quantities are
dimensionless does not justify saying that in the other cases adding one
(in a program) is an accident.
 
I hope you can see I know what you are getting at. It's the words I
don't like. It gives rise to statements that appear daft to me. I
don't mind "accidental" because the scare quotes tell me you don't mean
a literal accident.
 
--
Ben.
Malcolm McLean <malcolm.arthur.mclean@gmail.com>: Jun 28 07:22AM -0700

On Tuesday, 28 June 2022 at 14:08:00 UTC+1, David Brown wrote:
> If you find that confusing, then it is no wonder you are writing such a
> stream of muddled posts.
 
> As a general definition, a "pointer" is a way to access data indirectly.
 
A pointer is a way to access data indirectly.
But so is a reference.
So is a key.
 
But references and keys are not pointers.
In C and C++, it must be possible to take the address past the pointer.
Even in the case of a pointer to a scalar.
So a pointer does have to be an "address". Usually, a machine address of
course. In exceptional implementations you might want to argue about
what constitutes an "address" and what doesn't. But you must be able to
"go to the next address", so it's got to have number-like properties. It
can't be any system of indirection.
Siri Cruise <chine.bleu@yahoo.com>: Jun 27 04:58PM -0700

In article
<756f4d86-7d99-4047-a5ee-25705ecf265en@googlegroups.com>,
 
> If I try to compile the following with the GNU compiler (g++):
 
If worse comes to worse, you can always use cpp and persnickety
placements of #defines and #includes to get around these problems.
 
#define static auto
#include "idiot's stupid variable declarations"
#undef static
#define static static
 
Not ideal, but you can make anything work. If you do have to do
this nonsense, be sure to add comments why.
 
And if even worse comes to cesspit worse. m4 is widely available
and can be coerced with pinhead's hooks and chains to do what cpp
cannot.
 
--
:-<> Siri Seal of Disavowal #000-001. Disavowed. Denied. Deleted. @
'I desire mercy, not sacrifice.' /|\
Discordia: not just a religion but also a parody. This post / \
I am an Andrea Chen sockpuppet. insults Islam. Mohammed
Juha Nieminen <nospam@thanks.invalid>: Jun 28 06:51AM


> Given that the language of the standard says that it is unspecified
> whether values from the c prefixed headers (like cstddef) are declared
> in the global scope I would say this is not a bug.
 
AFAIK it's a compromise by the standard because they don't want to
overburden the standard library implementations, or something.
 
But I think it's a bad compromise. It's making the life of the standard
library implementors easier at the cost of making the life of C++
programmers harder. If you #include <cstddef> you cannot assume that
the names are in the global namespace but you have to take into
account that they could be. Good luck remembering every single
name in all the <c...> header files.
 
On top of that, IIRC you can't assume <stdlib.h> exists, so no luck
in trying to make sure that those names *are* in the global namespace.
(Although I think C++23 "undeprecates" those headers, so if you wait
for it to become widely supported you can make sure.)
"Alf P. Steinbach" <alf.p.steinbach@gmail.com>: Jun 28 10:19AM +0200

On 28 Jun 2022 08:51, Juha Nieminen wrote:
> account that they could be. Good luck remembering every single
> name in all the <c...> header files.
 
> On top of that, IIRC you can't assume <stdlib.h> exists,
 
Uh, where did you get that idea?
 
The ".h" headers were formally deprecated in 1998, the first standard,
because the committee unwisely, I'd say retardedly, assumed that the
intent of the clean "c..." headers would be fully supported and adopted.
 
That didn't happen, and will now never happen, since the committee
backed down a little from their high academic stance in 2011.
 
Likewise, it will never happen that the ".h" headers will disappear.
 
C++'s success is entirely founded on C compatibility, on leveraging C
libraries. That will not change. You can count on that much more than
you could count on core language features like `throw` specifications
and `export`-ed templates.
 
 
> so no luck
> in trying to make sure that those names *are* in the global namespace.
 
For C++11 names from the C library just include the relevant ".h"
header, very simple.
 
 
> (Although I think C++23 "undeprecates" those headers, so if you wait
> for it to become widely supported you can make sure.)
 
A simple remedy is to always include both of pair, "c..." and ".h".
 
That provides full guarantees about where the C++11 and earlier names
from the C library are defined, namely in both namespace `std` and the
global namespace. Guarantees that one can RELY on.
 
Unfortunately the committee didn't entirely give up their academic pipe
dream, and in C++14, C++17 and no doubt also in C++20, new names such as
`std::byte` and C11 esoteric math functions, were introduced in "c..."
headers only, with explicit exceptions that these names should not be
provided by ".h" headers. Sabotaging things just to score points for a
f...ing academic ideal. Politics everywhere, it's frustrating to have
the C++ standardization committee prioritize internal political goals to
that degree (one member even argued that Bjarne is to blame for all the
recent bad decisions, I think his words were "Bjarne is the problem").
 
 
- Alf
Paavo Helde <eesnimi@osa.pri.ee>: Jun 28 12:33PM +0300

27.06.2022 17:37 Frederick Virchanza Gotham kirjutas:
> If I try to compile the following with the GNU compiler (g++):
 
> #include <cstddef>
 
namespace MyGreatProject {
 
> class size_t {
 
> };
 
} // namespace
 
Problem solved.
"Öö Tiib" <ootiib@hot.ee>: Jun 28 02:49AM -0700

> libraries. That will not change. You can count on that much more than
> you could count on core language features like `throw` specifications
> and `export`-ed templates.
 
It wasn't retarded but carefully lobbied by likes of MicroSoft to get rid
of competitors like Watcom and Borland. Maybe some academics
were fooled but there never was hope. C++ headers are never defining
interface of module. So two modules written in C++ but compiled with
different version of compiler or different compiling options were never
designed to be possible to link together without using C headers as
mediator. Nothing to talk of other programming languages. So even if
there are some libraries written in C++ these either are open source
or pretend to be C libraries for to be useful.
Juha Nieminen <nospam@thanks.invalid>: Jun 28 01:25PM

>> On top of that, IIRC you can't assume <stdlib.h> exists,
 
> Uh, where did you get that idea?
 
I expressed it a bit poorly.
 
The <*.h> headers being deprecated means that it's not guaranteed that
they will exist in a future version of the standard.
 
So while yes, you can be sure that they exist eg. in C++11, you can't
be sure they will exist in some hypothetical future C++35 (and thus
your code may break if compiled in that mode).
 
OTOH the committee apparently relented with C++23 and will
"undeprecate" those headers, so you can use them without the fear
of them suddenly disappearing...
Juha Nieminen <nospam@thanks.invalid>: Jun 28 01:27PM


>> };
 
> } // namespace
 
> Problem solved.
 
Way to write confusing code inside that namespace...
David Brown <david.brown@hesbynett.no>: Jun 28 03:27PM +0200

On 28/06/2022 10:19, Alf P. Steinbach wrote:
> the C++ standardization committee prioritize internal political goals to
> that degree (one member even argued that Bjarne is to blame for all the
> recent bad decisions, I think his words were "Bjarne is the problem").
 
In all real compiler and library implementations of C++, the same
toolchain is both a C and a C++ compiler, and the library and headers
are shared where possible. But sometimes you just have a C compiler
without C++ - to the greatest possible extent, the C library and headers
of an implementation are a subset of the C++ library and headers. That
means the C++ committee do not mess with the contents of <math.h> or
<stdlib.h> except when it is unavoidable - it saves implementers
littering the files with "#ifdef __cplusplus ..." sections.
 
So when new maths functions were introduced, there were two options -
either the C committee also wanted them and they could go in <math.h>
(and therefore also <cmath>), or the C folks did not want them and the
C++ gang had to put them in their own header. It would, IMHO, have made
more sense to have a new C++ specific header with the new functions
rather than adding them to <cmath>, but I haven't bothered to follow the
discussions and reasoning behind the decision.
hester holt <hesterholt6@gmail.com>: Jun 27 09:37PM -0700

This is the PDF eBook version for Clinical Scenarios in Surgery - Decision Making and Operative Technique by Dimick
(Download link for cheap price) https://booksca.ca/library/ebook-clinical-scenarios-in-surgery-decision-making-and-operative-technique-by-dimick/
You received this digest because you're subscribed to updates for this group. You can change your settings on the group membership page.
To unsubscribe from this group and stop receiving emails from it send an email to comp.lang.c+++unsubscribe@googlegroups.com.

No comments: