storage for temporaries

S

S.Tobias

I'm examining the existence of temporary objects by looking at
their addresses. The trick is to create a structure that contains
an array as its first member. In an expression the array rvalue
is converted to a pointer to its first member. Since this address
is also the address of the array, and that is the address of the
structure, I conclude that this is also the address of the temporary
storage for the structure (r)value.

I'm aware of "Heisenberg effect", ie. in absence of an observer
(".a" operator) the actual behaviour of the operand expressions
might be different. Some behaviours are impossible to measure,
eg. in expression `x = f()' it's impossible to check if `f()'
returns directly into `x', or into a temporary (or other) object,
which is then copied into `x' (I could of course analyze the assembly
listing, but I want to rely only on visible behaviour).
I don't attempt to modify anything.

I would like you to answer my questions and review my conclusions.
The code is at the end, together with the output for como and gcc.
This code compiles only in C99 mode.

1. It is obvious that for some large temporary data a compiler
must sometimes reserve some memory. What does the Standard
say about storage for temporary values? Is it defined (C&V),
undefined or unspecified? What does the Standard say about
the output from my program?

2. Is the expression `f().a[0]' valid? Value of the result of
function call (or any other operator that yields an rvalue) is
accessed, but there's no sequence point, so there doesn't seem to
be an explicit UB in that area. OTOH it is undefined behaviour if
the pointer (f().a + 0) has invalid value (6.5.3.2p4). Even if for
some reason it is valid and there's no object, then it is undefined
behaviour if the lvalue `f().a[0]' does not designate an object when
evaluated (6.3.2.1p1). Anyway, it seems like the answer depends
on whether the temporary thingy for the rvalue is an object or not.

3. In case of sfunc_static() (below) the address of the temporary
could be the same as the static object, because modifying the result
of a fn call raises UB, therefore there is nothing to "protect".

4. Similar argument applies to "=" operator (address "val" could
be the same as that of "s1"). (Conditional and comma operators
in gcc output reuse the operand object storage for the value.)

5. Const object storage can always be reused as storage for its
value, because it is UB if that object is modified.


=== c_temp_obj.c ======================================
#include <stdio.h>

#define PRINT(msg, ptr) printf("%s: \t%p\n", msg, (void*)(ptr))

struct s { int a[1]; };

struct s sfunc()
{
struct s s = {0};
PRINT("inside fn", s.a); /* address of internal variable */
return s;
}


static struct s sfunc_static_obj = {0};

/* sfunc_static() is declared inline so that there is no need
to return through a temporary */
inline static
struct s sfunc_static()
{
PRINT("inside fn", sfunc_static_obj.a); /* address of the static object */
return sfunc_static_obj;
}

inline static
struct s sfunc_ptrarg(struct s *ps)
{
PRINT("inside fn", (*ps).a);
return *ps;
}

int main()
{
struct s s1 = {0}, s2 = {0};
const struct s sc = {0};

puts("\n*&:");
PRINT("s1", &s1);
PRINT("*&s1", (*&s1).a); /* control code, expected equal */

puts("\nfunction call:");
PRINT("fn return", sfunc().a); /* address of the temporary */

puts("\nfunction call (static object):");
PRINT("fn return", sfunc_static().a); /* address of the temporary */

puts("\nfunction call (through argument):");
PRINT("s1", s1.a);
PRINT("fn return", sfunc_ptrarg(&s1).a); /* address of the temporary */

puts("\nassignment operator:");
PRINT("s1", s1.a);
PRINT("s2", s2.a);
PRINT("val", (s1 = s2).a); /* address of the temporary */

puts("\nconditional operator:");
PRINT("s1", s1.a);
PRINT("?:", (1?s1:s2).a);

puts("\ncomma operator:");
PRINT("s1", s1.a);
PRINT("(,)", (0,s1).a);

puts("\ncomma operator, const object:");
PRINT("sc", sc.a);
PRINT("(,)", (0,sc).a);
return 0;
}
=== out.como ==========================================

*&:
s1: 0xbffffa44
*&s1: 0xbffffa44

function call:
inside fn: 0xbffffa00
fn return: 0xbffffa38

function call (static object):
inside fn: 0x8049830
fn return: 0xbffffa34

function call (through argument):
s1: 0xbffffa44
inside fn: 0xbffffa44
fn return: 0xbffffa30

assignment operator:
s1: 0xbffffa44
s2: 0xbffffa40
val: 0xbffffa2c

conditional operator:
s1: 0xbffffa44
?:: 0xbffffa28

comma operator:
s1: 0xbffffa44
(,): 0xbffffa24

comma operator, const object:
sc: 0xbffffa3c
(,): 0xbffffa20
=== out.gcc ===========================================

*&:
s1: 0xbffff224
*&s1: 0xbffff224

function call:
inside fn: 0xbffff1e0
fn return: 0xbffff218

function call (static object):
inside fn: 0x80499f4
fn return: 0xbffff214

function call (through argument):
s1: 0xbffff224
inside fn: 0xbffff224
fn return: 0xbffff210

assignment operator:
s1: 0xbffff224
s2: 0xbffff220
val: 0xbffff20c

conditional operator:
s1: 0xbffff224
?:: 0xbffff224

comma operator:
s1: 0xbffff224
(,): 0xbffff224

comma operator, const object:
sc: 0xbffff21c
(,): 0xbffff21c
 
L

Lawrence Kirby

I'm examining the existence of temporary objects by looking at
their addresses. The trick is to create a structure that contains
an array as its first member. In an expression the array rvalue
is converted to a pointer to its first member. Since this address
is also the address of the array, and that is the address of the
structure, I conclude that this is also the address of the temporary
storage for the structure (r)value.

I'm aware of "Heisenberg effect", ie. in absence of an observer
(".a" operator) the actual behaviour of the operand expressions
might be different. Some behaviours are impossible to measure,
eg. in expression `x = f()' it's impossible to check if `f()'
returns directly into `x', or into a temporary (or other) object,
which is then copied into `x' (I could of course analyze the assembly
listing, but I want to rely only on visible behaviour).
I don't attempt to modify anything.

I would like you to answer my questions and review my conclusions.
The code is at the end, together with the output for como and gcc.
This code compiles only in C99 mode.

1. It is obvious that for some large temporary data a compiler
must sometimes reserve some memory. What does the Standard
say about storage for temporary values?

Very little. However 6.5.2.2p5 says:

"... If an attempt is made to modify the result of a function call or to
access it after the next sequence point, the behavior is undefined."

Which means that this "temporary storage" doesn't have to last beyond the
next sequence point. That implies that a pointer to it becomes
indeterminate after the next sequence point.
Is it defined (C&V), undefined or unspecified?

The implication is that it can be accessed up to the next sequence point.
C never says how memory is allocated, the concepts of undefined behaviour
etc. relate to specific code and constructs not allocation techniques.
What does the Standard say about
the output from my program?

There is a squence point after the evaluation of printf()'s argumets just
before it is called. So the value of the pointer you pass is indeterminate
within printf() so the behaviour is undefined.
2. Is the expression `f().a[0]' valid? Value of the result of
function call (or any other operator that yields an rvalue) is
accessed, but there's no sequence point, so there doesn't seem to
be an explicit UB in that area. OTOH it is undefined behaviour if
the pointer (f().a + 0) has invalid value (6.5.3.2p4). Even if for
some reason it is valid and there's no object, then it is undefined
behaviour if the lvalue `f().a[0]' does not designate an object when
evaluated (6.3.2.1p1). Anyway, it seems like the answer depends
on whether the temporary thingy for the rvalue is an object or not.

6.3.2.1p3 certainly suggests that the f().a part of f().a[0] evaluates to
a pointer to the first element of an array object.
3. In case of sfunc_static() (below) the address of the temporary
could be the same as the static object, because modifying the result
of a fn call raises UB, therefore there is nothing to "protect".

The first question is whether they have to be distinct objects in the
abstract machine. That isn't entirely clear but lets assume yes. Then if
the caller had a way of testing the address of the temporary object
against the address of the static object then they would have to be
unequal. However as written that doesn't seem to be possible so you're
right they could be the same object. If the static object had external
linkage they would need to be different, or at least behave as if they are.
4. Similar argument applies to "=" operator (address "val" could
be the same as that of "s1"). (Conditional and comma operators
in gcc output reuse the operand object storage for the value.)

Other sources of non-lvalue structs are much less clear as 6.5.2.2p5
doesn't apply to them. It is also less clear that this temporary object
has to be different to s1 in the abstract machine.
5. Const object storage can always be reused as storage for its
value, because it is UB if that object is modified.

There is still the issue of testing the address of the const object
against the address of the temporary object.

....

Lawrence
 
N

Netocrat

Lawrence said:
I'm examining the existence of temporary objects by looking at
their addresses. The trick is to create a structure that contains
an array as its first member.
2. Is the expression `f().a[0]' valid? Value of the result of
function call (or any other operator that yields an rvalue) is
accessed, but there's no sequence point, so there doesn't seem to
be an explicit UB in that area. OTOH it is undefined behaviour if
the pointer (f().a + 0) has invalid value (6.5.3.2p4). Even if for
some reason it is valid and there's no object, then it is undefined
behaviour if the lvalue `f().a[0]' does not designate an object when
evaluated (6.3.2.1p1). Anyway, it seems like the answer depends
on whether the temporary thingy for the rvalue is an object or not.

I agree. Initially I misread it and asserted in the thread I started
yesterday that 6.8.6.4#3 implies that the return isn't an object, but
actually it implies the opposite:

6.8.6.4#3 states that "the value is converted ... to an object" when
the expression type is different to the return type, and by implication
when the type is the same.

So a function must return a temporary object that exists at least until
the next sequence point.
6.3.2.1p3 certainly suggests that the f().a part of f().a[0] evaluates to
a pointer to the first element of an array object.
3. In case of sfunc_static() (below) the address of the temporary
could be the same as the static object, because modifying the result
of a fn call raises UB, therefore there is nothing to "protect".

The first question is whether they have to be distinct objects in the
abstract machine. That isn't entirely clear but lets assume yes.

Agreed, it isn't clear. I can't see a good reason to assume yes though
(apart from conceptual clarity) - in what situation would it be useful
for the object addresses to compare differently?
Then if
the caller had a way of testing the address of the temporary object
against the address of the static object then they would have to be
unequal. However as written that doesn't seem to be possible

What's wrong with the following (and again, in what situation might
such a test be useful?):

sfunc_static_obj.a == sfunc_static().a
so you're
right they could be the same object.

4. Similar argument applies to "=" operator (address "val" could

Other sources of non-lvalue structs are much less clear as 6.5.2.2p5
doesn't apply to them. It is also less clear that this temporary object
has to be different to s1 in the abstract machine.


There is still the issue of testing the address of the const object
against the address of the temporary object.

I'd ask the same question in these cases as above - when would it be
important that the objects be different?
 
S

S.Tobias

Lawrence Kirby said:
Very little. However 6.5.2.2p5 says:

"... If an attempt is made to modify the result of a function call or to
access it after the next sequence point, the behavior is undefined."

Which means that this "temporary storage" doesn't have to last beyond the
next sequence point. That implies that a pointer to it becomes
indeterminate after the next sequence point.


The implication is that it can be accessed up to the next sequence point.

I agree it sort of suggests that the function return (and other
rvalues as well: "?:", ",") may "modifiable", but it isn't yet
a (strong) guarantee that there is an object (it says about
an _attempt_ to modify an rvalue, it doesn't mean yet that rvalues
may be modified or may designate objects at all).
C never says how memory is allocated, the concepts of undefined behaviour
etc. relate to specific code and constructs not allocation techniques.

What I meant is whether the semantics for the expressions were
defined/undefined/unspecified, not behaviour (although it affects
it in the end as well).
There is a squence point after the evaluation of printf()'s argumets just
before it is called. So the value of the pointer you pass is indeterminate
within printf() so the behaviour is undefined.

I agree (if at all we can talk about an rvalue object - see below).

But this is easily fixed in two ways:

#define PRINT(msg, ptr) printf("%s: \t%lld\n", msg, (long long)(void*)(ptr))
^^^^^^^^^^^
and then the behaviour is partly implementation defined, and we may
ask about the printed values, or at least whether the Standard guarantees
them to be equal (for the pairs printed in my program).

Or I might modify the program and test expressions like this:
s.a == (0,s).a
and the question is "What does the Standard guarantee about its result?".

(I won't change and post a new version of my program now, but I'll
have above in mind. To have a working version, let's assume PRINT
macro was changed.)
2. Is the expression `f().a[0]' valid? Value of the result of
function call (or any other operator that yields an rvalue) is
accessed, but there's no sequence point, so there doesn't seem to
be an explicit UB in that area. OTOH it is undefined behaviour if
the pointer (f().a + 0) has invalid value (6.5.3.2p4). Even if for
some reason it is valid and there's no object, then it is undefined
behaviour if the lvalue `f().a[0]' does not designate an object when
evaluated (6.3.2.1p1). Anyway, it seems like the answer depends
on whether the temporary thingy for the rvalue is an object or not.

6.3.2.1p3 certainly suggests that the f().a part of f().a[0] evaluates to
a pointer to the first element of an array object.

Now I'm not sure there is an object (storage) at all (in the abstract
machine). The Standard should _first_ state that there is a guaranteed
object there. I think that clause maybe also read as: Since there is no
storage, `f().a' is converted to a pointer which does not point to any
object; or: Since there is no storage, it is undefined to what `f().a'
is converted and thus invokes UB; it doesn't actually say there that
every array expression has a corresponding object.

6.2.4 defines three kinds of storage and defines when it is reserved.
[Note to others: There are significant changes between n869.txt and
the Standard in that part.]
All points of this clause have one thing in common: they describe
declarations of identifiers. In other places of the Standard storage
is defined for unnamed objects (eg. compound literals). Nowhere could
I find a description that could fit temporary thingies under discussion.


Some other thoughts of mine (not necessarily consistent with what
I have written above):

6.2.4p2:
# [...] The value of a pointer becomes indeterminate when
# the object it points to reaches the end of its lifetime.
This roughly means: "object, pointer, no-object, pointer becomes
indeterminate". Since/if there has never been any underlying object
for rvalue expression, the value `f().a' need not be necessarily
indeterminate (but the problem to what it is converted remains).
(Same logic for: void *p = (void*)1234; )

Suppose:
struct s { int a[2]; int m; }
^^^
If there is an underlying object (for the rvalue), would it mean
that this expression is valid?
( (struct s)(void*)(0,s).a )->m;
Above I circumvented the constraint that the operand for "&" must
be an lvalue, ie. this would be wrong:
(&(0,s))->m; //CV
although, if it were right, it would mean exactly the same thing.
(IOW: If a struct rvalue contains an array, it means that
the whole struct rvalue must have a corresponding object.)

Even if 6.3.2.1p3 is to imply that some storage has to be created,
it says only about its "initial element"; it doesn't strictly
imply that storage has to be reserved for other elements. While:
f().a[0];
might be fine, this might not be:
f().a[1];
 
S

S.Tobias

Netocrat said:
Lawrence said:
2. Is the expression `f().a[0]' valid? Value of the result of
function call (or any other operator that yields an rvalue) is
accessed, but there's no sequence point, so there doesn't seem to
be an explicit UB in that area. OTOH it is undefined behaviour if
the pointer (f().a + 0) has invalid value (6.5.3.2p4). Even if for
some reason it is valid and there's no object, then it is undefined
behaviour if the lvalue `f().a[0]' does not designate an object when
evaluated (6.3.2.1p1). Anyway, it seems like the answer depends
on whether the temporary thingy for the rvalue is an object or not.

I agree. Initially I misread it and asserted in the thread I started
yesterday that 6.8.6.4#3 implies that the return isn't an object, but
actually it implies the opposite:

6.8.6.4#3 states that "the value is converted ... to an object" when
the expression type is different to the return type, and by implication
when the type is the same.

Not so fast! That part says:

# 3 If a return statement with an expression is executed,
# the value of the expression is returned to the caller as the
# value of the function call expression. If the expression has
# a type different from the return type of the function in which
# it appears, the value is converted as if by assignment to an
# object having the return type of the function.135)

So it says that the value is converted into an object only
when its type is different than the return type. It doesn't actually
say that that object is later the underlying object for the returned
value (outside the function); it cannot - there's a sequence point
after return. It seems to be rather a formal procedure:
int f() {
long l = 1;
return l;
/* becomes: */
int temp;
temp = l;
return temp;
}
 
L

lawrence.jones

S.Tobias said:
# 3 If a return statement with an expression is executed,
# the value of the expression is returned to the caller as the
# value of the function call expression. If the expression has
# a type different from the return type of the function in which
# it appears, the value is converted as if by assignment to an
# object having the return type of the function.135)

So it says that the value is converted into an object only
when its type is different than the return type.

No it doesn't, you're misreading it. It says that the value is
converted in the same way as it would be converted if you assigned it
to an object, not that the value is actually converted to an object.

-Larry Jones

I'm not a vegetarian! I'm a dessertarian. -- Calvin
 
N

Netocrat

No it doesn't, you're misreading it. It says that the value is
converted in the same way as it would be converted if you assigned it
to an object, not that the value is actually converted to an object.

The problem is that it can be interpreted both ways, hence my wavering.
Inserting punctuation to show the association:

"the value is converted {as if by assignment to an object...}" is how
you are interpreting it and how I initially interpreted it.

"the value is converted {as if by assignment} to an object ..." is how
I later interpreted it.

In either case, as Stan points out, it is not a definitive statement of
the object status of the return and at best is a weak implication.
 
P

pete

Netocrat said:
The problem is that it can be interpreted both ways,
hence my wavering.
Inserting punctuation to show the association:

"the value is converted {as if by assignment to an object...}" is how
you are interpreting it and how I initially interpreted it.

"the value is converted {as if by assignment} to an object ..." is how
I later interpreted it.

In C, conversions are type conversions.
Values are assigned to objects,
not converted to objects.
In either case, as Stan points out,
it is not a definitive statement of
the object status of the return and at best is a weak implication.

It just means that func returns ((int)a).

int func(char a)
{
return a;
}
 
N

Netocrat

[Re what 6.8.6.4#3 implies about the object status of function returns]
In C, conversions are type conversions.
Values are assigned to objects,
not converted to objects.

You and Lawrence are right. The first interpretation is obviously the
only correct one.
It just means that func returns ((int)a).

Agreed. So the standard nowhere defines whether or not a function
returns an object.
 
J

Joe Wright

Netocrat said:
[Re what 6.8.6.4#3 implies about the object status of function returns]

In C, conversions are type conversions.
Values are assigned to objects,
not converted to objects.


You and Lawrence are right. The first interpretation is obviously the
only correct one.

It just means that func returns ((int)a).


Agreed. So the standard nowhere defines whether or not a function
returns an object.
From N869..

3.15
[#1] object
region of data storage in the execution environment, the
contents of which can represent values

[#2] NOTE When referenced, an object may be interpreted as
having a particular type; see 6.3.2.1.

It must be clear to you that a function cannot return a region of data
storage. Tell me it's clear. Functions return values, never objects.
 
N

Netocrat

Netocrat said:
pete said:
Netocrat wrote:

(e-mail address removed) wrote:

S.Tobias <[email protected]> wrote:


[Re what 6.8.6.4#3 implies about the object status of function returns]

So it says that the value is converted into an object only
when its type is different than the return type.

No it doesn't, you're misreading it. It says that the value is
converted in the same way as it would
be converted if you assigned it
to an object, not that the value is actually converted to an object.

The problem is that it can be interpreted both ways,
hence my wavering.
Inserting punctuation to show the association:

"the value is converted {as if by assignment to an object...}" is how
you are interpreting it and how I initially interpreted it.

"the value is converted {as if by assignment} to an object ..." is how
I later interpreted it.

In C, conversions are type conversions.
Values are assigned to objects,
not converted to objects.


You and Lawrence are right. The first interpretation is obviously the
only correct one.

It just means that func returns ((int)a).


Agreed. So the standard nowhere defines whether or not a function
returns an object.
From N869..

3.15
[#1] object
region of data storage in the execution environment, the
contents of which can represent values

[#2] NOTE When referenced, an object may be interpreted as
having a particular type; see 6.3.2.1.

It must be clear to you that a function cannot return a region of data
storage. Tell me it's clear. Functions return values, never objects.

It's not clear. In the original example, if sfunc().a is legal (where "a"
is an array), then "a" decays to a pointer and therefore the storage
location of the struct returned by sfunc() can be identified. So we have
(1) a region of data storage that has (2) a particular type - i.e. an
object. So no, it's not clear to me that functions never return objects.

I do believe it is wrong for the standard to allow this because I agree
that C tradition is that functions only return values.
 
O

Old Wolf

Netocrat said:
Joe said:
3.15
[#1] object
region of data storage in the execution environment, the
contents of which can represent values

[#2] NOTE When referenced, an object may be interpreted as
having a particular type; see 6.3.2.1.

It must be clear to you that a function cannot return a region of data
storage. Tell me it's clear. Functions return values, never objects.

It's not clear. In the original example, if sfunc().a is legal
(where "a" is an array), then "a" decays to a pointer

OK so far...
and therefore the storage
location of the struct returned by sfunc() can be identified.

How would you identify it? Recall Lawrence Kirby's quote:

"... If an attempt is made to modify the result of a function
call or to access it after the next sequence point, the behavior
is undefined."

So you can't printf this pointer.
You can't do a less-than or greater-than on it either (since
that requires two pointers to the same object, and here we
allegedly have no pointers to ojbects!).

ISTR that the standard says pointers of object type must point
to objects (or be null or indeterminate). Does that imply that
the return value must be an object -- or at least, that the
return value must contain objects?

(I'm happy to use /rvalue/ and /temporary object/ interchangeably;
is this just a language-lawyer issue?)
So we have (1) a region of data storage that has (2) a particular
type - i.e. an object. So no, it's not clear to me that functions
never return objects.

FWIW, in C++ the return value is an object (and has its constructor
and destructor invoked at the appropriate times).
 
N

Netocrat

Netocrat said:
Joe said:
3.15
[#1] object
region of data storage in the execution environment, the
contents of which can represent values

[#2] NOTE When referenced, an object may be interpreted as
having a particular type; see 6.3.2.1.

It must be clear to you that a function cannot return a region of data
storage. Tell me it's clear. Functions return values, never objects.

It's not clear. In the original example, if sfunc().a is legal
(where "a" is an array), then "a" decays to a pointer

OK so far...
and therefore the storage
location of the struct returned by sfunc() can be identified.

How would you identify it? Recall Lawrence Kirby's quote:

"... If an attempt is made to modify the result of a function
call or to access it after the next sequence point, the behavior
is undefined."

Substitute "must exist" for "can be identified" then. i.e. the pointer
value must exist, regardless of whether we can identify what it is.
So you can't printf this pointer.
Agreed.

You can't do a less-than or greater-than on it either (since
that requires two pointers to the same object, and here we
allegedly have no pointers to ojbects!).

Why can't you compare against an arbitrary object?

struct s { int a[1]; } s;
struct s sfunc();

if (s.a > sfunc().a)
puts("Function returned object in a higher memory location than our
object.");
else
puts("Function returned object in equal or lower memory location.");

The comparison could be changed to any other operator including ==,
although it is debatable whether that would be allowed to compare true.
ISTR that the standard says pointers of object type must point
to objects (or be null or indeterminate).
From memory, the only one you missed is one past the end of an array.
Does that imply that
the return value must be an object -- or at least, that the
return value must contain objects?
Yes.

(I'm happy to use /rvalue/ and /temporary object/ interchangeably;
is this just a language-lawyer issue?)

Yes. I can't imagine a realistic situation where it matters;
definitely not in any code I write. I'm only posting on the topic to
explore how the standard should be interpreted and how it formally
defines concepts that we might otherwise use in everyday language.
FWIW, in C++ the return value is an object (and has its constructor
and destructor invoked at the appropriate times).

I'm not familiar with the C++ standard at all, but the typical OO
concept of an object is much richer than the C concept.
 
O

Old Wolf

Netocrat said:
Substitute "must exist" for "can be identified" then. i.e. the pointer
value must exist, regardless of whether we can identify what it is.


Why can't you compare against an arbitrary object?

Because of 6.5.8#5 ("Relational operators") which is too large
to quote, but says informally that a relational comparison of
two pointers is undefined behaviour unless they point to the
same object (or one-past-the-end etc.)
struct s { int a[1]; } s;
struct s sfunc();

if (s.a > sfunc().a)

A good example of this UB.
As for a rationale, I suppose it is unclear how to compare
pointers on a system that doesn't have a flat memory model.
They could have at least made it unspecified behaviour though.
The comparison could be changed to any other operator including ==,
although it is debatable whether that would be allowed to compare true.

The rules for == are different, in that you can compare
pointers to different objects. However I think that this
pointer we are discussing, would not be allowed to compare
equal to anything. I suppose that it could be the same
memory location as the actual object in the function that
has just returned, but there is no way that legal code can
verify this (barring human introspection).
 

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,764
Messages
2,569,566
Members
45,041
Latest member
RomeoFarnh

Latest Threads

Top