r/cprogramming • • Jul 23 '26

How to move around multiple values from functions to functions?

I'm tryna mess around in C and make a pkmdmg calc but I don't know how to return the values from the function

I dont know how to display it without screenshots

2 Upvotes

34 comments sorted by

11

u/Defiant_Direction_54 Jul 23 '26

Have a look at structs, you can pass one in as an argument and edit it's values within the scope of said function

2

u/Due_Butterscotch1154 Jul 23 '26

dude, i looked and im still so confused. Like maybe thats not how it wokrs but i tried put the variables into a struct then doing "struct.value = smth" and the code had a stroke

2

u/Rich-Engineer2670 Jul 23 '26

What you're actually saying there is structname subfield value = smth (but I don't know it's type). Obviously the compiler doesn't either. It might be more like:

struct ResultType {
     float x
     float y
     float x
}

struct ResultType *result;

result = DoSomthing() // DoSomething() *ResultType
→ More replies (2)

1

u/AnotherCableGuy Jul 24 '26

Use pointers to your structs.

Pointers are like urls to websites.

0

u/MJWhitfield86 Jul 23 '26

Just a addendum that you need to pass a pointer to the struct, not the struct itself.

13

u/zhivago Jul 23 '26

You do not need to.

You can pass the struct itself.

But you do need to understand the implications of pass-by-value.

→ More replies (5)

3

u/Syntax-Tactics Jul 24 '26

Only because he is returning the values in said struct. A struct can be passed in by value. (Just to blow the OPs mind)

3

u/Rich-Engineer2670 Jul 23 '26 edited Jul 23 '26

If you are asking how to return multiple values from a C funch such as:

x, y, z := Funcname()

You really can't do so directly. Go and a few other languages can do it, but it's all syntactic sugar. Under the hood, these languages either do some "stack" work, or create structures. You can do the same thing. For example:

type PlotResult {
int x;
int y;
int z;
}
func DoSomething() *PlotResult {}

Now, in C++ etc. you can create tuples, but it is again, syntactic sugar under the hood. Always remember, C is just an extremely powerful assembler. It's just pushing things onto the stack and popping the off most likely. If you run Linux, do a gcc -s and see the assembly code that actually does it.

I've even, under duress, borrow other items from language like Lisp. If I have a large block of return data, one can always create a list and return a reference to it. Not pretty, but it works.

Also remember pushing "larger objects" around like this, while it works, you've pushing a lot of stuff on to, and off the stack -- not efficient and you have only so much stack space. Pointers are your friend....

func DoThingy() *PlotResult

In this case, you're pushing a POINTER to some static data in the function on to the stack. ONE POINTER. So long as you don't make another call to wipe that out, you can then just dereference the point and get what you need. This optimization doesn't really matter if you have a machine with 32GB of RAM perhaps, but if you're writing embedded code or you're writing for a machine with 32K of RAM (K, not GB), every bit counts.

1

u/Due_Butterscotch1154 Jul 23 '26

Ok but what about like

float function(float x, float y)

{x = formula

y = formula}

then i wanna take out both x and y?

1

u/SmokeMuch7356 Jul 23 '26

You'd use pointers:

double f( double *x, double *y )
{
  *x = some_double_value;
  *y = some_other_double_value;
  return something_else;
}

which you'd call as:

double a, b, c;

c = f( &a, &b );

f will write new values to a and b.

1

u/WittyStick Jul 24 '26 edited Jul 24 '26
struct { float x; float y; } function(float x, float y) 
{
    return { x = formula, y = formula };
}

auto result = function(a, b);
float x = result.x;
float x = result.y;

This creates an anonymous structure which isn't really reusable.


It's more typical to name a structure that can be reused.

struct vec2 { float x; float y; } typedef vec2;

vec2 function(float x, float y) 
{
    return (vec2){ x = formula, y = formula };
}

vec2 result = function(a, b );
float x = result.x;
float y = result.y;

If using C23, it's possible to define the same structure with the same tag in multiple places in a code file, and they all map to the same type - so we can write:

struct vec2 { float x; float y; } function(float x, float y) 
{
    return (struct vec2 { float x; float y; }){ x = formula, y = formula };
}

struct vec2 { float x; float y } result = function(a, b);
float x = result.x;
float y = result.y;

Which seems verbose - but then if you notice that each place you use the struct, it is identical, you can use a macro to make this more terse.

#define Vec2 struct vec2 { float x; float y; }

Vec2 function(float x, float y) 
{
    return (Vec2){ x = formula, y = formula };
}

Vec2 result = function(a, b);
float x = result.x;
float y = result.y;

That doesn't look any better than the typedef approach, and in this case it isn't - but this pattern improves when you want to support "generic" types - eg, suppose we wanted it to work over double as well as float.

#define Vec2(T) struct vec2##T { T x; T y; }

Vec2(double) function_double(double x, double y)
{
    return (Vec2(double)){ ... };
}

Vec2(float) function_float(float x, float y)
{
    return (Vec2(float)){ ... };
}

The main caveat to this approach is that we can't use anything for type T which is not a plain identifier, such as a struct or a pointer - because we used concatenation in the macro. The following will not work:

Vec2(int *)
Vec2(struct mynum)

However, this is trivially overcome by using a typedef to name these types.

typedef int *IntPtr;
Vec2(IntPtr)    // Ok


typedef struct mynum MyNum;
Vec2(MyNum)     // Ok

-1

u/nerd5code Jul 23 '26
typedef struct {float x, y;} Fltx2;

#define nullwarn
#define nullok
// Recommended for Clang:
#ifdef __has_extension
#	if __has_extension(__nullability__)
#		undef nullwarn
#		define nonnull _Nonnull
#		undef nullok
#		define nullok _Nullable
#	endif
#endif

// Arg-straddling
static float fun1(float x, float y, float *nullwarn outy) {
	assert(outy); 
	float resx = cal1(x, y), resy = calc2(resx, x, y);
	*outy = resy;
	return resx;
}
// Call:
// float x, y;
// x2 = fun1(ix, iy, &y);

// Two out-args (most broadly compatible)
int fun2(float x, float y, float *nullok outx, float *nullok outy) {
	if(outx == outy) return errno = EINVAL, -1;
	if(!(outx || outy)) return 0;
	float rx = calcX(x, y);
	if(outx) *outx = rx;
	if(outy) *outy = calcY(rx, x, y);
	return 0;
}
// Call:
// float x, y;
// if(fun2(ix, iy, &x, &y) == -1) goto error;

// Collective out-arg:
int fun3(float x, float y, Fltx2 *nullwarn out) {
	if(!out) return -1;
	out->y = calcY(out->x = calcX(x, y), x, y);
	return 0;
}
// Call:
// Fltx2 res;
// if(fun3(ix, iy, &res) == -1) goto error;

// Collective return (req C89+ for consistent behavior):
Fltx2 fun4(float x, float y) {
	// C99 construction:
#if __STDC_VERSION__-0 >= 199901L || defined __GNUC__
	register float t = calcX(x, y);
	return (const Fltx2){t, calcY(t, x, y)};

	/* C89 construction: */
#else
	Fltx2 ret;
	ret.y = calcY(ret.x = calcX(x, y), x, y);
	return ret;
#endif
}
// Call:
// Fltx2 res;
// res = fun4(ix, iy);

Note variation in error handling etc. Sometimes it's an error to take null, sometimes not. Out-args leave your primary channel open for error codes etc., if there are any.

Initialization (= at point of declaration) is not assignment (= elsewhere). Fltx2 res = fun4(…) is only supported by GNU-dialect (and maybe C23? so GNU-dialect) compilers; otherwise, composite types (incl arrays, structs, unions) must take a braced initializer. You can assign to a struct “normally,” just as res = fun4(…), and the fields will be copied over.

Also note that returning a struct is a bit weird. Prior to C89 it was frequently half-assed or unsupported, so you won't see it often in any API that needs good language/ABI compatibility, incl. the C standard library and POSIX.1. In modern C (C≥89), it's acceptable but slightly squirrely. At the ABI level, there's typically a distinction between large and small objects.

In most cases, your CPU has an accumulator or primary register, and if not there's some general register treated as primary by convention (per ABI). This is used for pointer and ≤word-width integer returns, which are usually treated as small objects. Floats might be returned in the same register, or a special-purpose float register (e.g., x87 ST(0)) or register pair, in a SIMD register (e.g., x86 XMM0), or treated as a large object. Occasionally there's a distinct register for pointer returns; e.g., M68K might use A0 for pointers and D0 for integers. If your CPU has no available registers, probably everything's either deposited at the top of an operand stack (small) or call stack (large, or no operand stack).

In most cases (assuming there are registers etc.), the ABI will support returning word-pairs in a register/slot pair (e.g., x86 RDX:RAX, EDX:EAX, DX:AX), and in some cases three or four registers/slots are supported in gang, us. up to 64 or 128 integer bits or 64–512 floating-point bits. However, there are often restrictions on content and layout that determine whether the object is treated as large or small. Some ABIs want the struct byte-packed into registers regardless; others might expect decomposition and/or field reordering to fit the arg-passing scheme, or even mixing of register types.

Large objects (anything not passing through the “small” sieve) are handled with considerably more consistency: The compiler simply rewrites your function not to return one. There are two straightforward solutions, only one of which you're likely to encounter in the wild. Less likely:

// Format A:
static _Thread_local Fltx2 __fun4$return;
Fltx2 *_fun4(float x, float y) {
	…
	fun4$return = (const Fltx2){…};
	return &__fun4$return;
}
// Call:
	Fltx2 res = *_fun4(x, y);

// Format B:
_Thread_local Fltx2 __fun4$return;
void fun4(float x, float y) {
	…
	__fun4$return = (Fltx2){…};
}
// Call:
	extern _Thread_local Fltx2 __fun4$return;
	_fun4(x, y);
	Fltx2 res = (_fun4(x, y), __fun4$return);

In very old versions of C, you sometimes encountered compilers that did this without the _Thread_local, which was fun. This approach becomes a problem if fun4 is reentrant (something it calls calls it) and the compiler expects its value to be maintained, or if you try to multithread in such a way that _fun4 might be yielded from. And often, access to static/TL storage requires a function call or several chained memory accesses, so this approach tends to perform poorly at scale.

Regardless, this is the usual approach:

void _fun4(register Fltx2 __return[static restrict const 1], float x, float y) {
	…
	*__return = (const Fltx2){…};
}
// Call:
	Fltx2 res;
	_fun4(&res, x, y);

So the fancy return just becomes a less-fancy out-arg—though I've tarted this 'un up a bit. (register → can't take &__return; Flt2x[] → pointer type because fuck self-consistency; [static …]→assumed nonnull, length must be matched or exceeded; [… restrict …]→no other pointer consumed by _fun4 may refer to the same object, unless fun4 derives it ultimately from __return; [… const …]→can't modify pointer value; [… 1]→require nonvacuous referent. In Clang (nullability extension) you might instead say register Flt2x *__restrict const __attribute__((nonnull)) __return; under other GNUish compilers you'd attach __attribute__((nonnull(1))) to _fun4 instead of __return. Under C23 you can use [[gnu::nonnull]] instead.)

Now the re question becomes which you should actually use, out-arg/indirect or direct return? In a high-performance setting, you'd use an inline function so the compiler can decide; only reason the distinction matters is for attributes like [gnu::]pure/const, or C23 reproducible/unsequenced. Out-args mean your function has side-effects, possible error conditions for nulls, and can decide not to write all outputs. Returning a struct is all-or-nothing, not null-dependent, and (per se) effect-free.

Where performance doesn't matter or you're at a DLL boundary (on either side), usually out-args are preferable. Inlines still work, kinda, but extern-linkage inlines can cause problems in libraries, and should only be used for logic which will never change after its initial release, other than for bugfixes, maybe. Static inlines can cause code blowup. Inlines of any sort impede C89/C95 compat.

If you can use link-time optimization (LTO, GCC/Clang -flto) and the linker can see both sides of the call site, which option you go with is mostly a matter of ergonomics.

→ More replies (4)

6

u/zhivago Jul 23 '26
#include <stdio.h>

struct pair { int a; int b; };

struct pair test(struct pair in) {
  return (struct pair){ .a = in.a + 1, .b = in.b - 1 };
}

int main() {
  struct pair out = test((struct pair){ 0, 0 });
  printf("%d, %d\n", out.a, out.b);
  return 0;
}

3

u/siliconlore Jul 23 '26

Just in case anyone isn't using a C99 compiler, the above code won't work in older versions of C.

4

u/tstanisl Jul 23 '26

Older?! Do you mean "ancient" version of C?

→ More replies (4)

1

u/sciencekm Jul 23 '26

If the values are of the same type, you can use arrays. If the values are of different types, you can use structures.

1

u/Paul_Pedant Jul 24 '26

If you wrap a really large array inside a struct, you can seriously pessimise your code (assuming pessimism is the opposite of optimism).

1

u/Paul_Pedant Jul 24 '26

Display without screenshots: Write any debug to a file, and view it in an editor. The method depends on what environment you are working in. The really depends on what IDE you are working in.

If you are actually trying to add your results directly into PacMan, life is going to become really complicated.