Never ever use a raw pointer when a smart pointer can do the same job

H

Hicham Mouline

.... my colleague says.

I disagree on the "Never".

Opinions about this "rule of thumb" welcome.

regards,
 
P

Pascal J. Bourguignon

Hicham Mouline said:
... my colleague says.
I disagree on the "Never".
Opinions about this "rule of thumb" welcome.

Never use a smart pointer when a garbage collector can do the same job.
 
V

Victor Bazarov

Hicham said:
... my colleague says.

I disagree on the "Never".

Opinions about this "rule of thumb" welcome.

What's the definition of "the same job"? They are for different jobs.
Can a screwdriver do a chisel's job? Could you drive a screw with a
chisel? What do you do if you don't have a light crowbar, but have a
strong screwdriver near-by? So, I say, bullsh!t. I use raw pointer
when I need to use a raw pointer and I use a smart pointer when I need
to use a smart pointer. If I don't have the facility at hand, I *might*
spend time implementing it, or I simply use what's available, in the
best manner possible, to achieve the goal in front of me.

Also, tell your colleague, the [counter-]saying is "Never say 'never'"
or "All generalizations are wrong". You may need to explain the word
"oxymoron" to him/her (without offending them).

V
 
N

Noah Roberts

Victor said:
What's the definition of "the same job"? They are for different jobs.
Can a screwdriver do a chisel's job? Could you drive a screw with a
chisel? What do you do if you don't have a light crowbar, but have a
strong screwdriver near-by? So, I say, bullsh!t. I use raw pointer
when I need to use a raw pointer and I use a smart pointer when I need
to use a smart pointer.

What purpose does a raw pointer serve?

Almost any case when you need a heap allocated data blob can be solved
by the use of raii objects such as std::vector or wrapping such
allocations in a smart pointer such as scoped_ptr, shared_ptr, or even
std::auto_ptr. If any of those object do what you need you should use
them because NOT using raii leaves you with all the problems raii answers.

Most times when you want to pass a raw pointer it's because you're
actually wanting a simple reference to some externally owned data. Most
of these cases should be resolved by using exactly what you want: a
reference. The only case when this is not possible is when you are
creating an object that:

a) needs an assignment operator that does a shallow copy (or anything
else where you'd override the reference without assigning to the object
you're referring to)

b) can't initialize the reference upon construction.

c) You're not allowed to ignore warnings and aren't allowed to override
the "assignment operator could not be created" warning (in which case a
reference will break the coding standard you're working in)

Note that not all uses of a pointer are solved by a smart pointer but
most uses ARE solved by the use of something besides a raw pointer.

There are exceptions to any standard. I think a standard that says,
"Wrap every raw pointer in an RAII object," is a good standard that
allows the team to dictate when to override (not the whim of some
stubborn developer who thinks they don't need RAII). The standard
given, "...when a smart pointer can do the same job," seems to maybe be
even better since it dictates exactly when to use a smart pointer and
when not to. On the other hand, it seems to leave question of
definition about when a smart pointer can do the same job.

I'd be willing to entertain arguments to the contrary but the project
I'm managing uses such a standard and I've yet to see a convincing
argument that it's a bad one. We have our exceptions when we need them
(such as working with WX) but generally wrap any pointer creation into
some sort of smart object that will delete it at the right time.
 
V

Victor Bazarov

Noah said:
Victor said:
What's the definition of "the same job"? They are for different jobs.
Can a screwdriver do a chisel's job? Could you drive a screw with a
chisel? What do you do if you don't have a light crowbar, but have a
strong screwdriver near-by? So, I say, bullsh!t. I use raw pointer
when I need to use a raw pointer and I use a smart pointer when I need
to use a smart pointer.

What purpose does a raw pointer serve?

Almost any case when you need a heap allocated data blob [..]

You answered your own question - it's not a heap allocated data blob (to
start, anyway).
Most times when you want to pass a raw pointer it's because you're
actually wanting a simple reference to some externally owned data. Most
of these cases should be resolved by using exactly what you want: a
reference. The only case when this is not possible is when you are
creating an object that:

a) needs an assignment operator that does a shallow copy (or anything
else where you'd override the reference without assigning to the object
you're referring to)

b) can't initialize the reference upon construction.

c) You're not allowed to ignore warnings and aren't allowed to override
the "assignment operator could not be created" warning (in which case a
reference will break the coding standard you're working in)

or d) the referred object is actually allowed to be *missing*. How'd
you do that with a reference?

A smart pointer is nothing but a wrapper around a raw pointer. There is
no escape, you *have to* use a raw pointer somewhere, even if you're the
one who implements that wrapper (or the smart pointer).
Note that not all uses of a pointer are solved by a smart pointer but
most uses ARE solved by the use of something besides a raw pointer.

"Most", "most", "most"... The key word. There goes the "never"...
There are exceptions to any standard. I think a standard that says,
"Wrap every raw pointer in an RAII object," is a good standard that
allows the team to dictate when to override (not the whim of some
stubborn developer who thinks they don't need RAII). The standard
given, "...when a smart pointer can do the same job," seems to maybe be
even better since it dictates exactly when to use a smart pointer and
when not to. On the other hand, it seems to leave question of
definition about when a smart pointer can do the same job.

I'd be willing to entertain arguments to the contrary but the project
I'm managing uses such a standard and I've yet to see a convincing
argument that it's a bad one. We have our exceptions when we need them
(such as working with WX) but generally wrap any pointer creation into
some sort of smart object that will delete it at the right time.

Delete what? An automatic object? A static object? A function? With
a raw pointer I have a choice what to pass to the function that can
expect "something or nothing", and that something can be basically
anything, not just a dynamically allocated blob. And don't tell me all
your objects exist on the heap. Mine don't.

V
 
B

BGB / cr88192

Noah Roberts said:
What purpose does a raw pointer serve?

when doing custom memory management;
when working with data which is "not an object" (loosely structured or
unstructured regions of memory, ...);
....

basically, not all tasks involve "objects", sometimes the task may involve
something with far less "identity" or "structure"...

AKA: raw pointers, for raw memory...


raw memory has not gone away, just as ASM has not gone away.
some tasks are best solved with raw memory, just as some tasks are best
solved with ASM (namely, those which if attempted to be done in C or C++
would tend to become some horrible monstrosity, or be essentially impossible
in the first place, and yes, some such cases exist...).


it is much like a typical car contains a spare tire and a cross-wrench...
will you typically need these while driving?...
probably not.

but should the minority case happen, and one has a flat or a blow-out, then
suddenly the need for such things will present itself...

likewise goes for code.
 
D

D. Frankinson

Hicham said:
... my colleague says.

I disagree on the "Never".

Opinions about this "rule of thumb" welcome.

What's the definition of "the same job"? They are for different jobs.
Can a screwdriver do a chisel's job? Could you drive a screw with a
chisel? What do you do if you don't have a light crowbar, but have a
strong screwdriver near-by? So, I say, bullsh!t. I use raw pointer
when I need to use a raw pointer and I use a smart pointer when I need
to use a smart pointer. If I don't have the facility at hand, I *might*
spend time implementing it, or I simply use what's available, in the
best manner possible, to achieve the goal in front of me.

Also, tell your colleague, the [counter-]saying is "Never say 'never'"
or "All generalizations are wrong". You may need to explain the word
"oxymoron" to him/her (without offending them).

V

Actually, you can use a chisel as a screwdriver, you just have to hit
very hard. Like as in, really hard.
 
J

James Kanze

... my colleague says.
I disagree on the "Never".
Opinions about this "rule of thumb" welcome.

It's one of the stupidest rules I've heard of. The rule should
be just the opposite: "never use a smart pointer when a raw
pointer will do the job". But even that's stupid, in a
way---smart pointers and raw pointers are different beasts, and
generally not interchangeable. Generally, if a smart pointer
will do the job, a raw pointer can't, and vice versa (unless you
have some very stupid and useless smart pointers).

In practice, in most application level software (at least in the
domains I've worked in), most pointers will be raw, because a
smart pointer would not do the job---at least not correctly.
But most is not all, and there will almost always be some cases
where the smart pointer is more appropriate as well.
 
N

Nick Keighley

Victor said:
Hicham Mouline wrote:
["Never ever use a raw pointer when a smart pointer can do the same
job"] my colleague says.
I disagree on the "Never".
Opinions about this "rule of thumb" welcome.
What's the definition of "the same job"?  They are for different jobs..
[...] I use raw pointer when I need to use a raw pointer and I use a
smart pointer when I need to use a smart pointer.

What purpose does a raw pointer serve?

to point to things...
Almost any case when you need a heap allocated data blob can be solved
by the use of raii objects such as std::vector [etc.]
 If any of those object do what you need you should use
them because NOT using raii leaves you with all the problems raii answers..

but RAII is not the only reson to use pointers.

polymorphism and threaded data structures

Most times

but not *all* times
when you want to pass a raw pointer it's because you're
actually wanting a simple reference to some externally owned data.  Most
of these cases should be resolved by using exactly what you want: a
reference.  The only case when this is not possible is when you are
creating an object that:

There are exceptions to any standard.

not in my universe
 I think a standard that says,
"Wrap every raw pointer in an RAII object," is a good standard that
allows the team to dictate when to override

so why not say "Wrap every raw pointer in an RAII object, except where
explicitly overridden by the Design Authority"

I think our trouble is I think absolutes are absolute and
you don't.

I don't think "never" is morally equivalent to "hardly ever"

"but that's the worse thing that could possibly happen,
but its worse than that!"
engineer on seeing a system log

<snip>
 
J

James Kanze

Never use a smart pointer when a garbage collector can do the
same job.

:)

I agree, but most pointers aren't implicated in any way in
memory management, so neither garbage collection nor smart
pointer is relevant. (That's not totally true, of course---even
in such cases, garbage collection can make the code more
robust.)
 
J

James Kanze

What purpose does a raw pointer serve?
Navigation.

Almost any case when you need a heap allocated data blob can
be solved by the use of raii objects such as std::vector or
wrapping such allocations in a smart pointer such as
scoped_ptr, shared_ptr, or even std::auto_ptr.

That's not the case in any of the applications I've worked on.
At the application level, lifetime is generally very
deterministic; objects are created and deleted as a result of
specific input from external sources. Using a scoped_ptr,
shared_ptr or an auto_ptr on any of these will result in a
double delete. (It's sometimes useful to use an auto_ptr during
the actual construction, until the transaction has completed, or
the object has been registered wherever it has to be
registered.)

Conceivably, there would be a use for a smart pointer which
works the opposite of garbage collection---deleting the object
causes the pointer to be null'ed. My site (when it was
accessible) had a ManagedPtr for that. In practice, however,
this is really only useful for 1->{0,1} relationships, and not
always for those (the object containing the pointer often has to
react to the fact that the pointed to object has disappeared); I
don't think I've used this smart pointer more than once or twice
in the more than ten years since I implemented it.
If any of those object do what you need you should use them
because NOT using raii leaves you with all the problems raii
answers.

RAII isn't really very appropriate for managing the lifetime of
dynamically allocated object, since RAII depends on lifetime
corresponding to a scope, and if the lifetime of an object
corresponds to a scope, then you don't allocate it dynamically.

In certain cases, shared_ptr can serve as a poor man's garbage
collector, but it's very error prone (compared to a real garbage
collector).
Most times when you want to pass a raw pointer it's because
you're actually wanting a simple reference to some externally
owned data. Most of these cases should be resolved by using
exactly what you want: a reference.

Unless it's an optional argument, where you might want to pass
a null pointer. But I agree with you in general---when passing
arguments, prefer references to pointers when possible. Most
pointers end up in the objects, themselves---class member
variables---where they are used for navigation. And must be
reseated, so references are out. A lot of the relationships
expressed by pointers are 1->n, which means some sort of
std::set< T* > said:
The only case when this is not possible is when you are
creating an object that:
a) needs an assignment operator that does a shallow copy (or
anything else where you'd override the reference without
assigning to the object you're referring to)
b) can't initialize the reference upon construction.

Or have to reseat it.
c) You're not allowed to ignore warnings and aren't allowed to
override the "assignment operator could not be created"
warning (in which case a reference will break the coding
standard you're working in)
Note that not all uses of a pointer are solved by a smart
pointer but most uses ARE solved by the use of something
besides a raw pointer.
There are exceptions to any standard. I think a standard that
says, "Wrap every raw pointer in an RAII object," is a good
standard that allows the team to dictate when to override (not
the whim of some stubborn developer who thinks they don't need
RAII). The standard given, "...when a smart pointer can do
the same job," seems to maybe be even better since it dictates
exactly when to use a smart pointer and when not to. On the
other hand, it seems to leave question of definition about
when a smart pointer can do the same job.
I'd be willing to entertain arguments to the contrary but the
project I'm managing uses such a standard and I've yet to see
a convincing argument that it's a bad one. We have our
exceptions when we need them (such as working with WX) but
generally wrap any pointer creation into some sort of smart
object that will delete it at the right time.

I'm curious as to the type of software you work on. You
obviously have some experience, but in all of the application
software I've seen (low level libraries are a different case),
dynamically allocated objects which have lifetimes corresponding
to those of a local variable are extremely rare---almost all
have lifetimes which begin and end in response to an external
stimulus. And almost all pointers are used for navigation, and
only navigation.
 
J

James Kanze

The same as (random access) iterators?

Ah, but those are smart pointers:). Not the type Noah was
thinking of, I think, but they're certainly more like smart
pointers than they are iterators. (In normal usage, "random
access iterator" is an oxymoron.)
 
A

Alf P. Steinbach

* James Kanze:
It's one of the stupidest rules I've heard of. The rule should
be just the opposite: "never use a smart pointer when a raw
pointer will do the job".

Here the reader will, IMHO, benefit from considering the context:

* James is one of the few who uses garbage collection in C++.
A typical programmer isn't.

* James is very much aware of where raw pointers "will do the
job", with a sophisticated understanding of what "the job" is.
A typical programmer isn't.

* James typically works with well-designed classes.
A typical programmer relates to classes that aren't that well
designed.

So although James' rule fits James, IMHO it doesn't fit the typical programmer.

For the typical programmer the main problem is to get the programmer to apply
automated and secure lifetime management, at all (smart pointers also solve
other problems, such as automated checking of availability, automated mutually
exclusive access, and so on, but such concerns are lesser issues).

But even that's stupid, in a
way---smart pointers and raw pointers are different beasts, and
generally not interchangeable. Generally, if a smart pointer
will do the job, a raw pointer can't, and vice versa (unless you
have some very stupid and useless smart pointers).

That statement relies on a sophisticated understanding of what "do the job" entails.

For a more simple understanding, such as "can be made to (apparently) work",
it's not the case that raw pointers can't do the job of smart pointers.

This follows from simple logic: at any point where a smart pointer would do
something extra, such as updating a reference count, that extra action, or
something equivalent and possibly more efficient, can be done explicitly with a
raw pointer.

Using a raw pointer where a smart pointer would be appropriate is the same as
basic "if" and "goto" being sufficient to express "for", "while", "do", "else"
and conditional operator. It's not impossible. When one considers only an
individual statement or two the "goto" based code is even simpler, because there
are only very simple "if" choices and very simple direct jumps, that's all (of
course at a higher level of abstraction the spaghetti code isn't simple).

And in the same way as with "goto", with a very narrow focus on individual
statements using a raw pointer might seem simpler, because there are only very
simple operations. And in the same way as with "goto", at a higher level of
abstraction, considering what the code accomplishes, the raw pointer based code
isn't simple. For in the same way as "goto", what the raw pointer lacks is (1)
restrictions on what you can do, and (2) automation of various actions.

In practice, in most application level software (at least in the
domains I've worked in), most pointers will be raw, because a
smart pointer would not do the job---at least not correctly.
But most is not all, and there will almost always be some cases
where the smart pointer is more appropriate as well.

"most application level software" is IMHO a severe over-generalization.


Cheers,

- Alf
 
J

James Kanze

* James Kanze:
Here the reader will, IMHO, benefit from considering the context:
* James is one of the few who uses garbage collection in C++.
A typical programmer isn't.

I don't always use garbage collection, for many reasons.
* James is very much aware of where raw pointers "will do the
job", with a sophisticated understanding of what "the job" is.
A typical programmer isn't.

A programmer who doesn't know what a raw pointer is, and what it
should be used for, simply isn't competent in C++.
* James typically works with well-designed classes.
A typical programmer relates to classes that aren't that well
designed.
So although James' rule fits James, IMHO it doesn't fit the
typical programmer.

Sorry, it's a rule I (and all of my collegues) use everywhere.

As I pointed out in the remainder of my posting, raw pointers
and smart pointers don't really overlap; there are very few
cases where both will do the job.
For the typical programmer the main problem is to get the
programmer to apply automated and secure lifetime management,

For the typical programmer, the main problem is to get him to
understand that fundamental design issues, like lifetime, must
be decided at the design level. And the problem is not to
manage lifetime with pointers (since that doesn't work), but to
make pointer use coincide with lifetime (which generally
involves some variant of the observer pattern).
at all (smart pointers also solve other problems, such as
automated checking of availability, automated mutually
exclusive access, and so on, but such concerns are lesser
issues).

I've used smart pointers for transaction management, in one
somewhat special case, and I use them from time to time to
manage mutually exclusive access (e.g. to a singleton). I use
std::auto_ptr rather religiously for communication between
threads---you can't access through the pointer once you've
passed it to the other thread. I've also used smart pointers
(my own invasive reference counted pointer) for memory
management in cases where garbage collection wasn't available,
but using smart pointers for this is extremely error prone, and
requires a good deal of precaution. There are lot's of cases
where smart pointers are appropriate, but except for the last,
they have nothing to do with memory management issues, and never
with object lifetime issues.
That statement relies on a sophisticated understanding of what
"do the job" entails.

If you don't know what the job is, you have a problem. A
serious problem, which can't be masked over by any simple
programming technique, like smart pointers.
For a more simple understanding, such as "can be made to
(apparently) work", it's not the case that raw pointers can't
do the job of smart pointers.
This follows from simple logic: at any point where a smart
pointer would do something extra, such as updating a reference
count, that extra action, or something equivalent and possibly
more efficient, can be done explicitly with a raw pointer.

Not reasonably. Strictly speaking, not at all; you need extra
code. But of course, you're just playing word games.

[...]
"most application level software" is IMHO a severe
over-generalization.

Yes and no. Show me a case of application level software where
a smart pointer is the most appropriate solution, and we can
discuss whether it is frequent enough to exclude the "most" in
my statement. (I very definitely know of a few cases where
smart pointers are appropriate, but they aren't common enough to
invalidate the "most".)
 
N

Nick Keighley

"Never ever use a raw pointer when a smart pointer can do the
same job"

[...] I use raw pointer when I
need to use a raw pointer and I use a smart pointer when I
need to use a smart pointer.
What purpose does a raw pointer serve?
Navigation.

Almost any case when you need a heap allocated data blob can
be solved by the use of raii objects such as std::vector or
wrapping such allocations in a smart pointer such as
scoped_ptr, shared_ptr, or even std::auto_ptr.

That's not the case in any of the applications I've worked on.
At the application level, lifetime is generally very
deterministic; objects are created and deleted as a result of
specific input from external sources.  Using a scoped_ptr,
shared_ptr or an auto_ptr on any of these will result in a
double delete.  (It's sometimes useful to use an auto_ptr during
the actual construction, until the transaction has completed, or
the object has been registered wherever it has to be
registered.)

what about sub-objects or resources? The high level object
may have a deterministic lifetime but the stuff it accumlates
during its lifetime has to be destroyed at the same time.
Is that a justification for a smart pointer or should you just
write your destructors correctly?

RAII isn't really very appropriate for managing the lifetime of
dynamically allocated object, since RAII depends on lifetime
corresponding to a scope, and if the lifetime of an object
corresponds to a scope, then you don't allocate it dynamically.

yes! I'd noticed that. Calls in a communications system don't
vanish at the end of a block!

Unless it's an optional argument, where you might want to pass
a null pointer.  But I agree with you in general---when passing
arguments, prefer references to pointers when possible.  Most
pointers end up in the objects, themselves---class member
variables---where they are used for navigation.  And must be
reseated, so references are out.  A lot of the relationships
expressed by pointers are 1->n, which means some sort of
container of pointers---std::vector< T* >, std::set< T* >,
std::map< IdType, T* >, etc.

yes. I was beginning to think I worked in some very odd domains!

I'm curious as to the type of software you work on.  You
obviously have some experience, but in all of the application
software I've seen (low level libraries are a different case),
dynamically allocated objects which have lifetimes corresponding
to those of a local variable are extremely rare---almost all
have lifetimes which begin and end in response to an external
stimulus.  And almost all pointers are used for navigation, and
only navigation.

my applications are pseudo-embedded; they very much respond to
events that are not determined by the scope structure of the
program.
 
A

Alf P. Steinbach

* James Kanze:
Conceivably, there would be a use for a smart pointer which
works the opposite of garbage collection---deleting the object
causes the pointer to be null'ed. My site (when it was
accessible) had a ManagedPtr for that. In practice, however,
this is really only useful for 1->{0,1} relationships, and not
always for those (the object containing the pointer often has to
react to the fact that the pointed to object has disappeared); I
don't think I've used this smart pointer more than once or twice
in the more than ten years since I implemented it.

It would be nice if objects that may encounter "unrecoverable failures" were
only accessible via such smart pointers.

The smart pointer would do all the validity checking, e.g. instead as with
current iostreams where every iostream operation checks the state and possibly
chooses to do nothing, the smart pointer would centralize the checking and throw
an exception.

It's an old idea. I once, many years ago, communicated with DDJ about writing an
article about this, but nothing more came of it than a tentative first attempt.
I don't know exactly why: the policy of DDJ seems to be to cut communication
when they're not interested (I think there was a fundamental change in DDJ when
they took up former Object Magazine, a wishy-washy buzzword publication, and
I'm not even sure that DDJ is still published in hard-copy, I haven't seen it
for ages). Anyway, those who are interested can find implementations on the net.
E.g. I posted a very limited implementation in this group just a few days ago.

But whereas I once saw GUI widgets (which are generally self-destroying) as a
main usage for such a pointer, I now hold the view that they're better accessed
simply via raw pointers. For the cases where a checking smart pointer would
detect access after destruction, are very few. In the majority of cases, for a
GUI widget the checking smart pointer would just be added complication.

However, files and socket objects and such, I think that's the proper usage. For
in contrast to GUI widgets these kinds of objects suffer from having natural
zombie states, states where nearly all operations are invalid. A checking smart
pointer can then centralize and enforce the validity (non-zombie) checking.


Cheers,

- Alf
 
H

Hicham Mouline

It's one of the stupidest rules I've heard of. The rule should
be just the opposite: "never use a smart pointer when a raw
pointer will do the job". But even that's stupid, in a
way---smart pointers and raw pointers are different beasts, and
generally not interchangeable. Generally, if a smart pointer
will do the job, a raw pointer can't, and vice versa (unless you
have some very stupid and useless smart pointers).
In practice, in most application level software (at least in the
domains I've worked in), most pointers will be raw, because a
smart pointer would not do the job---at least not correctly.
But most is not all, and there will almost always be some cases
where the smart pointer is more appropriate as well.

It is interesting to see so many different opinions about this.
Perhaps, the discussion can be fine tuned if I put down the use case
(trivial one) here:

We have a 3rd party application that accepts to load shared dynamic
libraries, this is on windows.
The application requires a number of entry points into the DLL. These are
"exported" functions, 2 of these
are

// with C linkage convention (ie , they use C and not C++)
int initialize(some_type* f);
void finalize();

1 translation unit where we implement this requirement looks like



extern "C" {

static const our_adapter* adapter=0; // global static raw pointer

int initialize(const some_type* f)
{
if (f==0)
{
return failure; /// 3rd application will ignore this dll
}
adapter = new our_adapter(*f); /// ctor never fails. If new fails, the
3rd party application crashes (we decided it's ok)
}

//// we axiomatize that initialize() is called once before finalize() and
that the 3rd party application calls these just once
void finalize()
{
delete adapter;
}

// other required function
int other_function()
{
adapter->do_something();
}

}


I would have had as a global variable the our_adapter itself, but its ctor
requires the "f", so I used a pointer instead.
Because of the exisiting symmetry between initalize and finalize, new'ing
and deleting seem to me appropriate.

My colleague insists I should use auto_ptr<our_adapter> instead of a raw
pointer here, and then said as a general rule,
never use a raw pointer when the smart pointer will work, hence the post.

could I have opinions about this particular case?

regards,
 
A

Alf P. Steinbach

* James Kanze:
For the typical programmer, the main problem is to get him to
understand that fundamental design issues, like lifetime, must
be decided at the design level. And the problem is not to
manage lifetime with [smart] pointers (since that doesn't work), but to
make pointer use coincide with lifetime (which generally
involves some variant of the observer pattern).

One might argue the same about using, e.g. "for" loops: it doesn't work as a
magic solution to the problem of designing loops.

The inference that therefore "goto" should be preferred, is, however, absurd.


Cheers,

- Alf
 

Ask a Question

Want to reply to this thread or ask your own question?

You'll need to choose a username for the site, which only take a couple of moments. After that, you can post your question and our members will help you out.

Ask a Question

Members online

No members online now.

Forum statistics

Threads
473,756
Messages
2,569,534
Members
45,007
Latest member
OrderFitnessKetoCapsules

Latest Threads

Top