What does the function actually do?
Does it have any side effects?
What are the valid values it can take? (i.e. it takes BaseObject* as an arg, but that type must be a OneLeggedYetiMonster*)
Most importantly, how will the function fail? (will it throw an exception, what are the error codes returned, and most importantly what arguments will cause those errors?)
Docs are most useful when they remove the need to go hunting through code for answers.
As an example, I've recently inherited a codebase where there are no return error codes, and every error is thrown instead. This means you have a bunch of member functions that look like this:
void load(const char* filename);
void update(float time);
Now some people claim their code is self-documenting, however in my experience, those people often produce some of the worst code interfaces known to mankind! (not always, but it's quite common imho). So whilst I have a pretty good idea what load/update do, and the arguments are self-explanatory, what isn't mentioned in the function names (or comments, because there aren't any), is that: passing in an out of range time value to update, causes all data to be destroyed, all external references to be invalidated, and an InvalidTimeValueRangeError to be thrown - unless of course you have set a kClamp flag on the object, in which case a TimeValueNotClampedError will be thrown. All of that is perfectly obvious from the function names. Obviously. :/
Sadly with this code I'm maintaining at the moment, because all of the functions return void, and no documentation exists for any of the functions (because it's "self-documenting"), I now find myself simply not trusting any of the code at all (even though it's fairly stable, and has been used in production for a few years). Every time I now make a call into the lib, I find myself digging through the code to make sure it's actually doing what I think it should be, before writing some doxygen comments to help the next person who ends up maintaining it! If I didn't go to those lengths, it would be extremely easy for me to make a commit, which would then blow up a week or two later due to some edge case I hadn't considered might occur.
The nice thing about doxygen comments is that it provides a contract between you, and the users of your code. It's kinda nice to be able to give someone a library, and then say to them "everything you need to know is in the docs". However it really sucks when that person has to constantly step through your code to find out what it's actually doing. Some people may say that in those cases the code is at fault (which is probably true), however I personally will accept a few WTF moments in an interface design, if it's actually explained how those WTF's work (because getting the job done quickly is usually your main aim). So if you have a function with 16 possible code paths, but only 3 of them will do what you expect - you should really prioritise explaining why those other 13 will cause a failure. If you have a simple getter/setter method, then it probably doesn't need as much doxygen TLC!
For a class, a higher level description about how to use it, and it's relationship with the rest of the system will be useful in the long run (and refer people to the relevant functions with detailed descriptions). I also like using the \name flags within a large class to group the functionality a little bit, i.e.
[source]
class Foo
{
public:
/// \name ctors
/// \brief probably don't need this comment
/// \brief default constuction
Foo();
/// \name getters / setters
/// \brief probably don't need this comment either
const char* getName() const;
/// \name collision detection
/// \brief probably needs some high level description here
/// \brief this could benefit from some info
bool someCollisionTest(SomeArg arg) const;
};
[/source]