Whats the deal with 'const'?

S

Snis Pilbor

Whats the point of making functions which take arguments of a form like
"const char *x"? It appears that this has no effect on the function
actually working and doing its job, ie, if the function doesn't write
to x, then it doesnt seem like the compiler could care less whether I
specify the const part. Quite the opposite, if one uses const
liberally and then later goes back and changes the functions, headaches
will inevitably occur as one tries to compile and the compiler gripes
because now suddenly you ARE writing to x, so you have to go back and
remove the const keyword, and this might mean painstakingly removing it
from dozens of lines if the function in question is deeply nested
amidst a family of functions that call eachother and all have 'const'
keywords.

Is const, in this context (ie passing const arguments to functions)....

1. A Java-like "babysitter" keyword based on the premise that all
programmers are idiots and must have their hands held at all times?
2. Used to alert the compiler that some kind of extra optimization is
possible, which wouldn't be possible if the data in question were
manipulated? And if so, exactly what sort of optimization would this
be?
3. About as useful as a comment, with no other purpose but as a little
yellow sticky note saying "this function should act nondestructively on
this particular thing"?

I always assumed it was some combination of 1 and 3 and so I tend to
never, ever use the thing...

Thanks, this is something I've always been curious about =)

Snis Pilbor
 
F

Frederick Gotham

Snis Pilbor posted:
Whats the point of making functions which take arguments of a form like
"const char *x"?


(Just for sake of pedantry:)

This is a non-const pointer variable which points to a const char.

It appears that this has no effect on the function
actually working and doing its job, ie, if the function doesn't write
to x, then it doesnt seem like the compiler could care less whether I
specify the const part.


The calling function cares:


void Func(char*);

int main(void)
{
char const c = 0;

Func(&c); /* Compiler ERROR */
}

Quite the opposite, if one uses const
liberally and then later goes back and changes the functions, headaches
will inevitably occur as one tries to compile and the compiler gripes
because now suddenly you ARE writing to x, so you have to go back and
remove the const keyword, and this might mean painstakingly removing it
from dozens of lines if the function in question is deeply nested
amidst a family of functions that call eachother and all have 'const'
keywords.


Revising whether a function alters its argument changes the very nature of
the function!

I take it you're new to "const"? "const" is brilliant.

Use it (almost) everywhere you can and you'll be left with very robust
code.

1. A Java-like "babysitter" keyword based on the premise that all
programmers are idiots and must have their hands held at all times?


I would consider "const" to have two main purposes:

(1) To aid in optimisation.
(2) To make code fail to compile if "constness" is violated.


<OFF-TOPIC>

Using function overloading in C++, you can write two totally different
functions -- one which alters its argument, and one which doesn't:

void Func(int *p);

void Func(int const *p);

2. Used to alert the compiler that some kind of extra optimization is
possible, which wouldn't be possible if the data in question were
manipulated? And if so, exactly what sort of optimization would this
be?


Consider:

int Func(int const x)
{
return x + 5;
}

Maybe the compiler will re-use the argument variable... ?

3. About as useful as a comment, with no other purpose but as a little
yellow sticky note saying "this function should act nondestructively on
this particular thing"?


It is reliable as such an indicator.


Don't forget:

It's undefined behaviour to alter a const object.
 
A

aragonsr

It helps with debugging. Putting const in the prototype allows you to 1)
send a "const" variable to the function or a nonconst variable to the
function. Without the const keyword, you could only send a nonconst variable
to the function. 2) when using references and pointers, it prevents the
function from modifying it which allows you to isolate bugs.

Although I believe references have to do with cpp, here is something that
you might find useful. Take this definition for example:

void object::func( const int* const variable ) const { ; }

the first const prevents the pointer from be moved around ( pointer
arithmetic ), the second const prevents the data to which the pointer points
to from being manipulated, and the third const prevents the function from
modifying member variables that belong to the class "object". The third one
is a cpp feature, but the other two should still hold true for C. Although I
believe the keyword "const" is a cpp keyword anyways and not a C keyword.
There is some ambituity there too I believe but I'm not sure.

I doubt there is any optimization, I don't know exactly what the compiler
produces when it comes to the const keyword so I don't know.
 
F

Frederick Gotham

aragonsr posted:

void object::func( const int* const variable ) const { ; }

the first const prevents the pointer from be moved around ( pointer
arithmetic ),


The first const prevents alteration of the data which is pointed to.

the second const prevents the data to which the pointer
points to from being manipulated,


The second const prevents alteration of the pointer variable itself.

I doubt there is any optimization, I don't know exactly what the
compiler produces when it comes to the const keyword so I don't know.


It's very likely to change:

unsigned const len = 5;

unsigned i = len;


into simply:


unsigned i = 5;


This would have an optimising effect on the resultant machine code, as,
rather than accessing "len" and subsequently loading a register with its
value, it would use a CPU instruction whereby it can pass 5 directly.
 
J

Jack Klein

Whats the point of making functions which take arguments of a form like
"const char *x"? It appears that this has no effect on the function
actually working and doing its job, ie, if the function doesn't write
to x, then it doesnt seem like the compiler could care less whether I
specify the const part. Quite the opposite, if one uses const
liberally and then later goes back and changes the functions, headaches
will inevitably occur as one tries to compile and the compiler gripes
because now suddenly you ARE writing to x, so you have to go back and
remove the const keyword, and this might mean painstakingly removing it
from dozens of lines if the function in question is deeply nested
amidst a family of functions that call eachother and all have 'const'
keywords.

The definition of a function parameter as "const char *x" defines that
argument as a pointer to one or more constant characters. The pointer
may be changed, any characters that it points to may not. This means
it may be called with a pointer to constant or non constant
characters. It is also a promise by the implementer of the function
that it will not attempt to modify any character(s) pointed to by 'x'.

Given a prototype of a function that accepts a pointer to const char,
a programmer may call that function with a pointer to one or more
chars that may not be modified, either an actual constant char or
array of constant chars, or a string literal.

Or it may be that the data pointed to is not a literal or constant,
but the caller needs it to keep its present value for further
processing.
Is const, in this context (ie passing const arguments to functions)....

1. A Java-like "babysitter" keyword based on the premise that all
programmers are idiots and must have their hands held at all times?

Making an object parameter constant is something like babysitting.
Some pedants here seem to like it, but it is only for the use of the
programmer writing the function. Example:

int some_func(const int x)
{
/* body */
}

The programmer is expressing his intent not to change the value of the
int parameter 'x' inside the function, and is instructing the compiler
to warn him if he does so. It is not part of the function's
interface, and completely useless information to a caller of the
function, because the 'x' the function receives is a copy of the
caller's value, and changes made to it inside this function cannot
have any effect on any object in the caller.

Some people like this because it prevents a certain type of error,
namely when an argument is modified early in a function when code
later in a function expects it to retain the original value.

My response to that would be that if a function is long or complex
enough that such a mistake is easy to make and easy to overlook, the
function should be split into two or more smaller, simpler functions.
2. Used to alert the compiler that some kind of extra optimization is
possible, which wouldn't be possible if the data in question were
manipulated? And if so, exactly what sort of optimization would this
be?

The C standard does not define optimizations. There might well be
optimizations possible with data pointed to by a pointer to const, but
that is an implementation detail and off-topic here.
3. About as useful as a comment, with no other purpose but as a little
yellow sticky note saying "this function should act nondestructively on
this particular thing"?

No, it is far, far more useful than a comment, at least when talking
about pointers to const objects. Especially when used properly.
I always assumed it was some combination of 1 and 3 and so I tend to
never, ever use the thing...

Thanks, this is something I've always been curious about =)

Consider:

int my_strlen1(const char *x)
{
int count = 0;
while (*x)
{
++count;
++x;
}
return count;
}

int my_strlen2(char *x)
{
int count = 0;
while (*x)
{
++count;
++x;
}
return count;
}

int main(void)
{
const char *str = "Hello, World!";
int l1 = my_strlen1(str);
int l2 = my_strlen2(str);
puts(str);
return 0;
}

Your compiler should issue a diagnostic when you attempt to call
my_strlen2 with a pointer to const. And that's important, because the
pointer 'str' points to a string literal, and modifying a string
literal produces undefined behavior.

If you modify my_strlen1, say like this:

int my_strlen1(const char *x)
{
int count = 0;
while (*x)
{
++count;
*x = '?';
++x;
}
return count;
}

....the compiler will issue a diagnostic. To remove the diagnostic you
must remove the const qualifier. If your code is organized properly,
that is the same prototype is in scope at the definition and all calls
to a function, removing the const qualifier from the prototype will
cause a diagnostic when the caller sends a pointer to a string literal
to the function. And this is a good thing, because passing a string
literal to a function that attempts to modify it is a serious error
and causes undefined behavior.
 
S

Snis Pilbor

Frederick said:
Snis Pilbor posted:
(snip)



Revising whether a function alters its argument changes the very nature of
the function!
(snip)

Not to be argumentative, but consider the following two versions of the
same function:

void print_first_5( char *x )
{
char buf[6];
int i;

for ( i = 0; i < 5; i++ )
buf = x;

buf[5] = '\0';
printf( buf );
}

void print_first_5( char *x )
{
char tmp;

tmp = x[5];
x[5] = '\0';
printf( x );
x[5] = tmp;
}

As far as I can tell, the 2nd is both faster and takes less memory.
The two do the exact same thing, assuming that printf itself acts
nondestructively. If I used const to tell the compiler that
print_first_5 acts nondestructively on x in the 2nd version, the
compiler would think I was mistaken even though I'm correct. The point
is, the change can hardly be said to "change the very nature of this
function", and if this were deeply nested in a family of functions
using const arguments, a massive headache would arise.

S.P.
 
F

Frederick Gotham

Jack Klein posted:

The C standard does not define optimizations. There might well be
optimizations possible with data pointed to by a pointer to const, but
that is an implementation detail and off-topic here.


Yes, that must be the exact reason why we have a "register" keyword.

I suppose "inline" has nothing to do with optimisation either.
 
F

Frederick Gotham

Snis Pilbor posted:
Not to be argumentative, but consider the following two versions of the
same function:

void print_first_5( char *x )
{
char buf[6];
int i;

for ( i = 0; i < 5; i++ )
buf = x;

buf[5] = '\0';
printf( buf );
}



#include <stdio.h>
#include <string.h>

void PrintFirst5(char const *p)
{
char static buf[5+1]; /* Auto null terminator */

memcpy(buf,p,5);

puts(buf);
}

void print_first_5( char *x )
{
char tmp;

tmp = x[5];
x[5] = '\0';
printf( x );
x[5] = tmp;
}


#include <stdio.h>
#include <string.h>

void PrintFirst5(char *p)
{
char * const register p_last = p + 5;

char const tmp = *p_last;

*p_last = 0;

puts(p);

*p_last = tmp;
}

As far as I can tell, the 2nd is both faster and takes less memory.


Indeed, however the array which it accesses must be non-const.

Consider:

PrintFirst5("carpenter");

If I used const to tell the compiler that
print_first_5 acts nondestructively on x in the 2nd version, the
compiler would think I was mistaken even though I'm correct.


You are mistaken.

You alter the data. Just because you later restore it to its original state
doesn't mean there was no alteration in the first place.

By your logic, if I was caught shoplifting, I could say "I was only
borrowing it, and was going to restore it to its original position before
anyone noticed".

The point is, the change can hardly be said to "change the very nature
of this function", and if this were deeply nested in a family of
functions using const arguments, a massive headache would arise.


The second form of the function is "clever".

You have changed the way the function works.

Perhaps the most fundamental aspect of a function, and the aspect which
should be documented clearly, is "whether it alters its arguments".

<OFF-TOPIC>

In C++, you could get the best of both worlds by using function overloading
to supply two functions: one which can work with const data, and one which
is optimised to work with non-const data.

</OFF-TOPIC>


Get the hang of using "const" and you'll realise it's the best thing since
sliced bread.
 
F

Frederick Gotham

Frederick Gotham posted:

#include <stdio.h>
#include <string.h>

void PrintFirst5(char const *p)
{
char static buf[5+1]; /* Auto null terminator */

memcpy(buf,p,5);

puts(buf);
}


I should have written the function parameter as:

char const * const p


void PrintFirst5(char *p)
{
char * const register p_last = p + 5;

char const tmp = *p_last;

*p_last = 0;

puts(p);

*p_last = tmp;
}


I should have written the function parameter as:

char * const p
 
T

Tim Prince

Jack said:
int my_strlen1(const char *x)
{
int count = 0;
while (*x)
{
++count;
++x;
}
return count;
}

int my_strlen2(char *x)
{
int count = 0;
while (*x)
{
++count;
++x;
}
return count;
}

int main(void)
{
const char *str = "Hello, World!";
int l1 = my_strlen1(str);
int l2 = my_strlen2(str);
puts(str);
return 0;
}
Current compilers exhibit a wide variety of behaviors in this situation.
Even within the same compiler family, one compiler may not have any
diagnostic available, and will link and run without problem, and another
may fail, particularly if the C code is compiled under C++.
I filed a problem report against a compiler for linux x86-64 which has
no available diagnostic or failure mode, while its Windows x64 sibling
runs into trouble later in the compilation.
With gcc 4.2, I saw a change in behavior between -m64 and -m32, where
the original problem is diagnosed directly only in -m64 mode, and the
consequences depend on context (even though the string is never
re-written). This contributed to my difficulty in convincing a bug
report team of the desirability of the diagnostic.
The f2c translator, even in "ansi" mode, generates legacy style code
which passes string constants without using "const" in the prototypes,
and that has only recently begun to fail.
 
R

REH

Snis Pilbor said:
Not to be argumentative, but consider the following two versions of the
same function:

void print_first_5( char *x )
{
char buf[6];
int i;

for ( i = 0; i < 5; i++ )
buf = x;

buf[5] = '\0';
printf( buf );
}

void print_first_5( char *x )
{
char tmp;

tmp = x[5];
x[5] = '\0';
printf( x );
x[5] = tmp;
}

As far as I can tell, the 2nd is both faster and takes less memory.
The two do the exact same thing, assuming that printf itself acts
nondestructively. If I used const to tell the compiler that
print_first_5 acts nondestructively on x in the 2nd version, the
compiler would think I was mistaken even though I'm correct. The point
is, the change can hardly be said to "change the very nature of this
function", and if this were deeply nested in a family of functions
using const arguments, a massive headache would arise.


First, I would output the characters directly and not copy them, nor modify
the original array. Secondly, the convenience and safety of not
unnecessarily modifying the input parameter outweighs any performance you
think you are losing. Unless it is a real issue (and since you are writing
to output, I seriously doubt it), don't worry so much about which is
"faster." By not modifying the string, the function can be used with both
modifiable and non-modifiable strings, and in this case, that probably
better than any perceived loss in speed.


REH
 
S

Skarmander

Frederick said:
Jack Klein posted:




Yes, that must be the exact reason why we have a "register" keyword.

I suppose "inline" has nothing to do with optimisation either.
Your sarcasm is misplaced; optimization is indeed not the primary intent of
"const". By that reasoning you might call static typing an optimization. It
may allow optimization, but that's not its primary benefit.

Moreover, the standard really *doesn't* define optimizations, it merely
standardizes certain hints to the compiler (which is free to ignore them).
I've seen plenty of compilers that all but ignore the "register" keyword,
for example, because they can do a far better job of register allocation
than any human programmer could (especially across different platforms). The
"register" keyword is a holdover from less sophisticated times.

Compilers that support "inline" tend to respect it (and some even have a
corresponding "noinline" keyword), since determining what functions to
inline is not something that compilers are perfect at yet (and may never
become, since in many cases profiling is required).

The mere fact that the standard gives these optimization hints uniform names
and guarantees that programs can use them should not be taken as an
indication that the standard is in the business of telling implementations
how to optimize. In fact, that would be anathema to the C philosophy of
leaving things unsaid that don't need to be said.

S.
 
E

Eric Sosman

Snis said:
Frederick said:
Snis Pilbor posted:
(snip)




Revising whether a function alters its argument changes the very nature of
the function!
(snip)


Not to be argumentative, but consider the following two versions of the
same function:

void print_first_5( char *x )
{
char buf[6];
int i;

for ( i = 0; i < 5; i++ )
buf = x;

buf[5] = '\0';
printf( buf );
}

void print_first_5( char *x )
{
char tmp;

tmp = x[5];
x[5] = '\0';
printf( x );
x[5] = tmp;
}

As far as I can tell, the 2nd is both faster and takes less memory.
The two do the exact same thing, assuming that printf itself acts
nondestructively. If I used const to tell the compiler that
print_first_5 acts nondestructively on x in the 2nd version, the
compiler would think I was mistaken even though I'm correct. The point
is, the change can hardly be said to "change the very nature of this
function", and if this were deeply nested in a family of functions
using const arguments, a massive headache would arise.


Neither version is any damned good; no decent programmer
would write either one of them. A few flaws:

- Try the first version with the argument "X". What happens
when it tries to read elements [2] through [4] of this
two-element array?

- Try the second version with the argument "X". What happens
when it stores a zero in element [5] of this two-element
array? (Even if it doesn't segfault immediately, how sure
are you that whatever got clobbered isn't used by printf()?)

- Try either version with the argument "%solved". Somebody
doesn't know how to use printf() safely ...

- Try the second version with "Hello, world!" on an
implementation that puts string literals in read-only
memory. (Extra credit: defend your "I'm correct" claim.)

Arguments can be made for and against aspects of a programming
language, but arguments based on broken code carry little weight.
 
M

Malcolm

Snis Pilbor said:
Is const, in this context (ie passing const arguments to functions)....

1. A Java-like "babysitter" keyword based on the premise that all
programmers are idiots and must have their hands held at all times?
2. Used to alert the compiler that some kind of extra optimization is
possible, which wouldn't be possible if the data in question were
manipulated? And if so, exactly what sort of optimization would this
be?
3. About as useful as a comment, with no other purpose but as a little
yellow sticky note saying "this function should act nondestructively on
this particular thing"?

I always assumed it was some combination of 1 and 3 and so I tend to
never, ever use the thing...
const is an addition to C.
It adds no functionality, and it doesn't really help the compiler to
optimise though someone might be able to come up with examples using pointer
aliasing or something similar.

It is a combination of 1 and 3. It is perfectly possible to write correct
code without const, unless you are an idiot programmer. So for idiot
programmers it might useful to be told that they are trying to write to a
const-qualified object.
Consider this function

void copy_string(char *str1, char *str2);

by looking at the prototype, you cannot tell which parameter is which.

void copy_string(const char *str1, char *str2);

tells you that the first parameter must be the source, so it is the other
way round to strcpy(). That's obviously handy.

The reason a lot of people don't like const is "const-poisoning". Once an
object is declared const, everything derived for it must be const, even it
is isn't really a constant.

The second problem is this

struct employee
{
char *name;
float salary;
};

void payroll(const employee *emp)
{

/* evil hacker */
strcpy(emp[0]->name, "Fred the Hacker");
pay(&emp[0]);
}
 
C

Chris Torek

[The OP, writing about "const" qualifiers, asked whether const-qualified
arguments were...]

Your sarcasm is misplaced; optimization is indeed not the primary intent of
"const". By that reasoning you might call static typing an optimization. It
may allow optimization, but that's not its primary benefit.

Indeed, it is quite a bit worse than this. C's "const" qualifiers
rarely allow any optimization at all!

Consider the following function:

int zog(const char *p) {
int result;

for (result = 0; *p; result++)
if (otherfunc())
break;
return result;
}

The loop inside this function repeatedly inspects *p. Well, we
claimed that *p was "const", so it will not change, right? Thus
the compiler can rewrite this as:

result = 0;
if (*p) {
while (otherfunc() == 0)
result++;
}
return result;

right?

Alas, wrong:

#include <stdio.h>

char buf[100];

int otherfunc(void) {
buf[42]--;
return 1;
}

int main(void) {
buf[42] = 12;

printf("%d\n", zog(&buf[42]));
return 0;
}

Each call to otherfunc() decrements buf[42], and *p inside zog is
just another name for buf[42]. The loop must in fact re-test *p
each time, and the code above must print 12.

In other words, even though zog() does not change *p, something
*else* might change it, despite the "const". Const does not mean
constant; it means "read-only". Read-only, but changing each time,
in this case.

In most (but not all) cases, "const" helps only when it is plainly
obvious that the variable has not changed, in which case, any decent
C compiler can substitute in the unchanged variable:

void zog2(void) {
const int i = 2;

while (i == 2)
other2();
}

Since nothing takes i's address here, it is impossible for i to
change. The const only helps make it clear that this is still
an infinite loop even if we pass &i to other2():

while (i == 2)
other2(&i);

Here, if i is *not* const, other2() can change it, but if i *is*
const, other2() cannot change it (without producing undefined
behavior, anyway).

The ugliest (in my opinion anyway) situation of all occurs when
C functions are forced to cast away "const"-ness from pointers in
their return values:

char *strchr(const char *s, int lookfor0) {
char lookfor = lookfor0;

for (; *s != c; s++)
if (*s == 0)
return NULL;
return (char *)s;
}

Fortunately, this mostly occurs in "legacy" routines like strchr(),
which the implementor (i.e., me :) ) has to write for the user
(i.e., you) -- most programmers are not forced to do this.
 
B

Bill Pursell

Chris Torek wrote, in reference to const being used
to aid optimization:
C's "const" qualifiers
rarely allow any optimization at all!

Consider the following function:

int zog(const char *p) {
int result;

for (result = 0; *p; result++)
if (otherfunc())
break;
return result;
}

[Where otherfunc() manipulates a global that p
references.]

Isn't this the point of the restrict keyword? eg, your
function doesn't allow
int zog(const char *restrict p),
but if otherfunc didn't tweak *p, you could prototype
it that way and reap the benefits of the optimization.
 
L

lovecreatesbeauty

Malcolm said:
The second problem is this

struct employee
{
char *name;
float salary;
};

void payroll(const employee *emp)

? void payroll(const employee **emp)
{

/* evil hacker */
strcpy(emp[0]->name, "Fred the Hacker");

? strcpy(emp->name, "Fred the Hacker");
pay(&emp[0]);
}

void payroll(const employee *emp);

In this case, the const keyword qualifies the object it points to as
constant. The object `name' is that one pointed by emp, so this is an
error:
/*(*emp).name = "C";*/

The object `*name' is pointed by name, not emp. That const does not
prevent following from being done:
*(*emp).name = 'c';

? void payroll(const employee **emp)

So does the const keyword applying to multiple-level pointer.
/*(**emp).name = "C";*/
*(**emp).name = 'c';

lovecreatesbeauty
 
C

Chris Torek

Chris Torek wrote, in reference to const being used
to aid optimization:

[Where otherfunc() manipulates a global that p
references.]

Isn't this the point of the restrict keyword?

Yes. It turns out that "char *restrict p", or perhpas
"const char *restrict p", means what most people thought
"const char *p" was going to mean.
eg, your function doesn't allow
int zog(const char *restrict p),
but if otherfunc didn't tweak *p, you could prototype
it that way and reap the benefits of the optimization.

Indeed. (By "prototype" I assume you mean "both declare and
define".) The "optimization" -- hoisting the *p test out of the
loop -- needs to takes place during compilation of the function
zog() itself, hence the the "restrict" has its main effect inside
the definition, rather than at some external prototype. Of course,
it is a good idea to make sure that every prototype declaration
for every function always matches the actual definition of the
function.
 
G

Guest

Chris said:
Chris Torek wrote, in reference to const being used
to aid optimization:

[Where otherfunc() manipulates a global that p
references.]

Isn't this the point of the restrict keyword?

Yes. It turns out that "char *restrict p", or perhpas
"const char *restrict p", means what most people thought
"const char *p" was going to mean.

Although the below code is definitely poor style, is any rule actually
violated?

void f(const int *restrict p) {
*(int *) p = 0;
}
int main(void) {
int i = 1;
f(&i);
return i;
}
 
M

Malcolm

lovecreatesbeauty said:
The second problem is this

struct employee
{
char *name;
float salary;
};

void payroll(const employee *emp)

? void payroll(const employee **emp)
{

/* evil hacker */
strcpy(emp[0]->name, "Fred the Hacker");

? strcpy(emp->name, "Fred the Hacker");
Should be a dot.
Silly mistake, but easy to make. Or get rid of the [0].
The point is the hacker is allowed to insert his own name into the employee
list and, presumably, get the salary paid into his own account, despite the
fact that the distrustful high level programmer thinks the structure is
const.
pay(&emp[0]);
}

void payroll(const employee *emp);

In this case, the const keyword qualifies the object it points to as
constant. The object `name' is that one pointed by emp, so this is an
error:
/*(*emp).name = "C";*/

The object `*name' is pointed by name, not emp. That const does not
prevent following from being done:
*(*emp).name = 'c';

? void payroll(const employee **emp)

So does the const keyword applying to multiple-level pointer.
/*(**emp).name = "C";*/
*(**emp).name = 'c';

lovecreatesbeauty
 

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

Forum statistics

Threads
473,743
Messages
2,569,478
Members
44,899
Latest member
RodneyMcAu

Latest Threads

Top