The other solutions in this thread suffer from the RelativeDir-based ../.. problems and having to set things manually on each source file.
Not to mention, they wreck /MP. Any solution which specifies an exact .obj for %(ObjectFileName) will result in a different /Fo for each .cpp file (to map it to a specific .obj file) passed to CL.exe and thus Visual Studio can't batch them. Without batching several .cpp files with identical commandlines (including /Fo) the /MP can't work.
Here's a new approach. This works on vs2010 through to vs2015 at least. Add this to your vcxproj in the <project>
<!-- ================ UNDUPOBJ ================ -->
<!-- relevant topics -->
<!-- https://stackoverflow.com/questions/3729515/visual-studio-2010-2008-cant-handle-source-files-with-identical-names-in-diff/26935613 -->
<!-- https://stackoverflow.com/questions/7033855/msvc10-mp-builds-not-multicore-across-folders-in-a-project -->
<!-- https://stackoverflow.com/questions/18304911/how-can-one-modify-an-itemdefinitiongroup-from-an-msbuild-target -->
<!-- other maybe related info -->
<!-- https://stackoverflow.com/questions/841913/modify-msbuild-itemgroup-metadata -->
<UsingTask TaskName="UNDUPOBJ_TASK" TaskFactory="CodeTaskFactory" AssemblyFile="$(MSBuildToolsPath)\Microsoft.Build.Tasks.v4.0.dll">
<ParameterGroup>
<OutputDir ParameterType="System.String" Required="true" />
<ItemList ParameterType="Microsoft.Build.Framework.ITaskItem[]" Required="true" />
<OutputItemList ParameterType="Microsoft.Build.Framework.ITaskItem[]" Output="true" />
</ParameterGroup>
<Task>
<Code><![CDATA[
//general outline: for each item (in ClCompile) assign it to a subdirectory of $(IntDir) by allocating subdirectories 0,1,2, etc., as needed to prevent duplicate filenames from clobbering each other
//this minimizes the number of batches that need to be run, since each subdirectory will necessarily be in a distinct batch due to /Fo specifying that output subdirectory
var assignmentMap = new Dictionary<string,int>();
HashSet<string> neededDirectories = new HashSet<string>();
foreach( var item in ItemList )
{
//solve bug e.g. Checkbox.cpp vs CheckBox.cpp
var filename = item.GetMetadata("Filename").ToUpperInvariant();
//assign reused filenames to increasing numbers
//assign previously unused filenames to 0
int assignment = 0;
if(assignmentMap.TryGetValue(filename, out assignment))
assignmentMap[filename] = ++assignment;
else
assignmentMap[filename] = 0;
var thisFileOutdir = Path.Combine(OutputDir,assignment.ToString()) + "/"; //take care it ends in / so /Fo knows it's a directory and not a filename
item.SetMetadata( "ObjectFileName", thisFileOutdir );
}
foreach(var needed in neededDirectories)
System.IO.Directory.CreateDirectory(needed);
OutputItemList = ItemList;
ItemList = new Microsoft.Build.Framework.ITaskItem[0];
]]></Code>
</Task>
</UsingTask>
<Target Name="UNDUPOBJ">
<!-- see stackoverflow topics for discussion on why we need to do some loopy copying stuff here -->
<ItemGroup>
<ClCompileCopy Include="@(ClCompile)"/>
<ClCompile Remove="@(ClCompile)"/>
</ItemGroup>
<UNDUPOBJ_TASK OutputDir="$(IntDir)" ItemList="@(ClCompileCopy)" OutputItemList="@(ClCompile)">
<Output ItemName="ClCompile" TaskParameter="OutputItemList"/>
</UNDUPOBJ_TASK>
</Target>
<!-- ================ UNDUPOBJ ================ -->
And then modify <project> so that it reads:
<Project InitialTargets="UNDUPOBJ" ...
The result will be something like myproj/src/a/x.cpp and myproj/src/b/x.cpp compiling to Debug/0/x.obj and Debug/1/x.obj. RelativeDirs arent employed and so aren't a problem.
Additionally, in this case, there will be only two different /Fo passed to CL.exe: Debug/0/ and Debug/1/. Consequently, no more than two batches will get issued to CL.exe, allowing the /MP to work more efficiently.
Other approaches would be basing the .obj subdirectories on the .cpp subdirectories, or making the .obj filename contain some memento of the original .cpp directory so that you can readily see a .cpp->.obj mapping, but those result in more /Fo and therefore less batching. Future work could dump a mapping file for quick reference, perhaps.
See this for more details on /MP and batching : MSVC10 /MP builds not multicore across folders in a project
I've tested this in production for quite a while on vs2010 and vs2015 on a variety of toolchains. It seems bulletproof, but there's always a chance it may interact badly with other msbuild customizations or exotic toolchains.
Starting in vs2015, if you get a warning "warning MSB8027: Two or more files with the name of X.cpp will produce outputs to the same location" then you can add this to your project or msbuild files:
<PropertyGroup Label="Globals"><IgnoreWarnCompileDuplicatedFilename>true</IgnoreWarnCompileDuplicatedFilename></PropertyGroup>
See more at https://connect.microsoft.com/VisualStudio/feedback/details/797460/incorrect-warning-msb8027-reported-for-files-excluded-from-build and How to suppress specific MSBuild warning
UPDATE SEP 2019:
I believe there is a problem with the above where the ItemList can be arbitrarily ordered resulting in reassignments of a given source file to varying numbered directories. Thus whenever the source file list changes, duplicate copies of the object may appear. This may also cause dependency tracking to get a bit messed up, since the input->output mapping can get changed surprisingly so that the timestamps make no sense.
We could fix this by hashing a sorted ItemList and prefixing that to the output directory, but this will create extra rebuilds and a lot of litter.
This could be refined by dropping a file listing input->output mappings, and having the mapping of a source file to destination numbered-directory be a one-time only event (until the project is cleaned).
I have yet to investigate this more deeply.